多个 Thread 实例共用一个 Runnable,这些线程的 run 方法相同,可以共享相同的数据
但是存在线程同步问题
public class RunnableTest implements Runnable
{
private int ticket = 10;
public void run()
{
while (true)
{
if (ticket > 0)
{
System.out.println(Thread.currentThread().getName() + "售出" + ticket + "号票");
ticket--;
}
else System.exit(0);
}
}
public static void main(String[] args)
{
RunnableTest rt = new RunnableTest();
Thread t1 = new Thread(rt, "1号窗口");
Thread t2 = new Thread(rt, "2号窗口");
t1.start();
t2.start();
}
}
1号窗口售出10号票
1号窗口售出9号票
1号窗口售出8号票
1号窗口售出7号票
2号窗口售出7号票
2号窗口售出5号票
1号窗口售出6号票
2号窗口售出4号票
1号窗口售出3号票
2号窗口售出2号票
1号窗口售出1号票
匿名类可以方便的访问方法的局部变量,但是必须声明为 final,因为匿名类和普通局部变量生命周期不一致
jdk7 中已不再需要显示声明为 final,实际上被虚拟机自动隐式声明了
public static void main(String[] args)
{
new Thread( )
{
public void run( )
{
//内容
}
}.start( );
new Thread(new Runnable( )
{
public void run( )
{
//内容
}
}).start( );
}
创建 Callable 的实现类,并冲写 call() 方法,该方法为线程执行体,并且该方法有返回值
创建 Callable 实现类的实例,并用 FutuerTask 类来包装 Callable 对象,该 FutuerTask 封装了 Callable 对象 call() 方法的返回值
实例化 FutuerTask 类,参数为 FutuerTask 接口实现类的对象来启动线程
通过 FutuerTask 类的对象的 get() 方法来获取线程结束后的返回值
public class CallableTest implements Callable<Integer>
{
//重写执行体 call( )
public Integer call( ) throws Exception
{
int i = 0;
for (; i < 10; i++)
{
//
}
return i;
}
public static void main(String[] args)
{
Callable call = new CallableTest( );
FutureTask<Integer> f = new FutureTask<Integer>(call);
Thread t = new Thread(f);
t.start( );
//得到返回值
try
{
System.out.println("返回值:" + f.get( ));
}
catch (Exception e)
{
e.printStackTrace( );
}
}
}
返回值:10
线程执行体:run()
启动线程:start()
Thread 类方法
方法 | 描述 |
---|---|
public final void setName(String name) | 改变线程名称 |
public final void setPriority(int priority) | 设置优先级 |
public final void setDaemon(boolean on) | 设为守护线程,当只剩下守护线程时自动结束 |
public final boolean isAlive() | 测试线程是否处于活动状态 |
public static void yield() | 暂停当前线程(回到就绪状态) |
public static void sleep(long millisec) | 进入休眠状态 |
public final void join() | 暂停当前线程,等待调用该方法线程执行完毕 |
public final void join(long millisec) | 暂停当前线程指定时间 |
public static Thread currentThread() | 返回对当前正在执行的线程对象的引用 |
就绪状态:
运行状态:就绪状态经过线程调度进去运行状态
阻塞状态:
死亡状态:run 方法执行完毕
graph TB
T(新线程)--start方法-->A(就绪状态)
A--线程调度-->B(运行状态)
B--yield方法-->A
B--sleep方法-->D(阻塞:休眠)
B--wait或join方法-->E(阻塞:wait池)
B--未获得锁-->F(阻塞:lock池)
B--run方法执行完-->C(死亡状态)
D--时间到-->A
E--notify方法-->F
F--获得锁-->A
保证程序原子性、可见性、有序性的过程
基于加锁争用的悲观并发策略
使用 synchronized 可以锁住某一对象, 当其他线程也想锁住该对象以执行某段代码时,必须等待已经持有锁的线程释放锁
释放锁的方式有互斥代码执行完毕、抛出异常、锁对象调用 wait 方法
不同的使用方式代表不同的锁粒度
创建 Lock 锁
ReentrantLock 实现了 Lock 接口, Lock lock = new ReentrantLock()
Lock 含义
使用 lock() 方法表示当前线程占有 lock 对象
释放该对象要显示掉用 unlock() 方法 ,多在 finally 块中进行释放
trylock 方法
Lock 的线程交互
通过 lock 对象得到一个 Condition 对象,Condition condition = lock.newCondition()
调用这个Condition对象的:await,signal,signalAll 方法
示例
public class LockTest
{
public static void log(String msg)//日志方法
{
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date = new Date( );
String dateStr = sdf.format(date);
System.out.println(dateStr + " " + Thread.currentThread( ).getName( ) + " " + msg);
}
public static void main(String[] args)
{
Lock lock = new ReentrantLock( );
new Thread("t1")
{
public void run( )
{
boolean flag = false;
try
{
log("线程已启动");
log("尝试占有lock");
flag = lock.tryLock(1, TimeUnit.SECONDS);
if (flag)
{
log("成功占有lock");
log("执行3秒业务操作");
Thread.sleep(3000);
}
else
{
log("经过1秒钟尝试,占有lock失败,放弃占有");
}
}
catch (InterruptedException e)
{
e.printStackTrace( );
}
finally
{
if (flag)
{
log("释放lock");
lock.unlock( );
}
}
log("线程结束");
}
}.start( );
try
{
//先让 t1 先执行两秒
Thread.sleep(2000);
}
catch (InterruptedException e1)
{
e1.printStackTrace( );
}
new Thread("t2")
{
public void run( )
{
boolean flag = false;
try
{
log("线程启动");
log("尝试占有lock");
flag = lock.tryLock(1, TimeUnit.SECONDS);
if (flag)
{
log("成功占有lock");
log("执行3秒的业务操作");
Thread.sleep(3000);
}
else
{
log("经过1秒钟的尝试,占有lock失败,放弃占有");
}
}
catch (InterruptedException e)
{
e.printStackTrace( );
}
finally
{
if (flag)
{
log("释放lock");
lock.unlock( );
}
}
log("线程结束");
}
}.start( );
}
}
2019-11-07 15:50:01 t1 线程已启动
2019-11-07 15:50:01 t1 尝试占有lock
2019-11-07 15:50:01 t1 成功占有lock
2019-11-07 15:50:01 t1 执行3秒业务操作
2019-11-07 15:50:03 t2 线程启动
2019-11-07 15:50:03 t2 尝试占有lock
2019-11-07 15:50:04 t2 经过1秒钟的尝试,占有lock失败,放弃占有
2019-11-07 15:50:04 t2 线程结束
2019-11-07 15:50:04 t1 释放lock
2019-11-07 15:50:04 t1 线程结束
synchronized 和 Lock 区别
非阻塞同步是一种基于冲突检测和数据更新的乐观并发策略
原子操作
actomic 类的使用
public class ThreadTest
{
static int value1 = 0;
static AtomicInteger value2 = new AtomicInteger(0);//原子整型类
public static void main(String[] args)
{
for (int i = 0; i < 100000; i++)
{
new Thread( )
{
public void run( )
{
value1++;
}
}.start( );
new Thread( )
{
public void run( )
{
value2.addAndGet(1);//value++的原子操作
}
}.start( );
}
while (Thread.activeCount( ) > 2)
{
Thread.yield( );
}
System.out.println(value1);
System.out.println(value2);
}
}
99996
100000
如果一个方法不涉及共享数据,那么他天生就是线程安全的
可以在代码执行的任何时刻中断它,转而去执行另外一段代码,在控制权返回之后,原来的程序不会出现任何的错误
一个方法返回结果是可以预测的,输入了相同的数据,就能返回相同的结果,那这个方法就具有可重入性,也就是线程安全的
栈封闭是一种可重用代码
多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量保存在虚拟机栈中,属于线程的私有区域,所以不会出现线程安全性
public class ThreadTest
{
static void add( )
{
int value = 0;
for (int i = 0; i < 1000; i++)
{
value++;
}
System.out.println(value);
}
public static void main(String[] args)
{
ExecutorService threadPool = Executors.newCachedThreadPool( );
threadPool.execute(( ) -> add( ));
threadPool.execute(( ) -> add( ));
threadPool.shutdown( );
}
}
1000
1000
把共享数据的可见范围限制在同一个线程之内,即便无同步也能做到避免数据争用
使用 java.lang.ThreadLocal 类来实现线程本地存储功能
public class ThreadLocalDemo
{
public static void main(String[] args)
{
ThreadLocal threadLocal1 = new ThreadLocal( );
Thread t1 = new Thread(( ) ->
{
threadLocal1.set(1);
try
{
Thread.sleep(3000);
}
catch (InterruptedException e)
{
e.printStackTrace( );
}
System.out.println(threadLocal1.get( ));
});
Thread t2 = new Thread(( ) -> threadLocal1.set(2));
t1.start( );
t2.start( );
}
}
1
ThreadLocal 原理
public static void main(String[] args)
{
Object o1 = new Object( );
Object o2 = new Object( );
Thread t1 = new Thread( )
{
public void run( )
{
synchronized (o1)//占有 o1
{
System.out.println("t1 已占有 O1");
try
{
Thread.sleep(1000);//停顿1000毫秒,另一个线程有足够的时间占有 o1
}
catch (InterruptedException e)
{
e.printStackTrace( );
}
System.out.println("t1 试图占有 o2");
System.out.println("t1 等待中");
synchronized (o2)
{
System.out.println("t1 已占有 O2");
}
}
}
};
Thread t2 = new Thread( )
{
public void run( )
{
synchronized (o2) //占有 o2
{
System.out.println("t2 已占有 o2");
try
{
Thread.sleep(1000);//停顿1000毫秒,另一个线程有足够的时间占有 o2
}
catch (InterruptedException e)
{
e.printStackTrace( );
}
System.out.println("t2 试图占有 o1");
System.out.println("t2 等待中");
synchronized (o1)
{
System.out.println("t2 已占有 O1");
}
}
}
};
t1.start( );
t2.start( );
}
t1 已占有 O1
t2 已占有 o2
t1 试图占有 o2
t1 等待中
t2 试图占有 o1
t2 等待中
Object 类方法
方法 | 描述 |
---|---|
wait() | 线程进入等待池 |
notify() | 唤醒等待当前线程锁的线程 |
notifyAll() | 唤醒所有线程,优先级高的优先唤醒 |
为什么这些方法设置在 Object 对象上?
表面上看,因为任何对象都可以加锁
底层上说,java 多线程同步的 Object Monitor 机制,每个对象上都设置有类似于集合的数据结构,储存当前获得锁的线程、等待获得锁的线程(lock set)、等待被唤醒的线程(wait set)
生产者消费者模型
public class ProducerAndConsumer
{
public static void main(String[] args)
{
Goods goods = new Goods();
Thread producer = new Thread()//生产者线程
{
public void run()
{
while (true) goods.put();
}
};
Thread consumer = new Thread()//消费者线程
{
public void run()
{
while (true) goods.take();
}
};
consumer.start();
producer.start();
}
}
class Goods//商品类
{
int num = 0;//商品数目
int space = 10;//空位总数
public synchronized void put()
{
if (num < space)//有空位可放,可以生产
{
num++;
System.out.println("放入一个商品,现有" + num + "个商品," + (space - num) + "个空位");
notify();//唤醒等待该锁的线程
}
else//无空位可放,等待空位
{
try
{
System.out.println("没有空位可放,等待拿出");
wait();//进入该锁对象的等待池
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
public synchronized void take()
{
if (num > 0)//有商品可拿
{
num--;
System.out.println("拿出一个商品,现有" + num + "个商品," + (space - num) + "个空位");
notify();//唤醒等待该锁的线程
}
else///等待生产产品
{
try
{
System.out.println("没有商品可拿,等待放入");
wait();//进入该锁对象的等待池
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
}
没有商品可拿,等待放入
放入一个商品,现有1个商品,9个空位
放入一个商品,现有2个商品,8个空位
拿出一个商品,现有1个商品,9个空位
放入一个商品,现有2个商品,8个空位
放入一个商品,现有3个商品,7个空位
放入一个商品,现有4个商品,6个空位
拿出一个商品,现有3个商品,7个空位
放入一个商品,现有4个商品,6个空位
···
线程的启动和结束都是比较消耗时间和占用资源的,如果在系统中用到了很多的线程,大量的启动和结束动作会严重影响性能
线程池很像生产者消费者模式,消费的对象是一个一个的能够运行的任务
设计思路
实现一个线程池
public class ThreadPool
{
int poolSize;// 线程池大小
LinkedList<Runnable> tasks = new LinkedList<Runnable>();// 任务容器
public ThreadPool(int poolSize)
{
this.poolSize = poolSize;
synchronized (tasks)//启动 poolSize 个任务执行者线程
{
for (int i = 0; i < poolSize; i++)
{
new ExecuteThread("执行者线程 " + i).start();
}
}
}
public void add(Runnable r)//添加任务
{
synchronized (tasks)
{
tasks.add(r);
System.out.println("加入新任务");
tasks.notifyAll();// 唤醒等待的任务执行者线程
}
}
class ExecuteThread extends Thread//等待执行任务的线程
{
Runnable task;
public ExecuteThread(String name)
{
super(name);
}
public void run()
{
System.out.println("启动:" + this.getName());
while (true)
{
synchronized (tasks)
{
while (tasks.isEmpty())
{
try
{
tasks.wait();
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
task = tasks.removeLast();
tasks.notifyAll(); // 允许添加任务的线程可以继续添加任务
}
System.out.println(this.getName() + " 接到任务");
task.run();//执行任务
}
}
}
public static void main(String[] args)
{
ThreadPool pool = new ThreadPool(3);
for (int i = 0; i < 5; i++)
{
Runnable task = new Runnable()//创建任务
{
public void run()//任务内容
{
System.out.println(Thread.currentThread().getName()+" 执行任务");
}
};
pool.add(task);//加入任务
try
{
Thread.sleep(1000);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
}
main 加入新任务
启动:执行者线程 0
执行者线程 0 接到任务
执行者线程 0 执行任务
启动:执行者线程 1
启动:执行者线程 2
main 加入新任务
执行者线程 2 接到任务
执行者线程 2 执行任务
main 加入新任务
执行者线程 2 接到任务
执行者线程 2 执行任务
java 线程池类
默认线程池类 ThreadPoolExecutor 在 java.util.concurrent 包下
ThreadPoolExecutor threadPool= new ThreadPoolExecutor(10, 15, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
/*
第一个参数 int 类型, 10 表示这个线程池初始化了 10 个线程在里面工作
第二个参数 int 类型, 15 表示如果 10 个线程不够用了,就会自动增加到最多 15个 线程
第三个参数 60 结合第四个参数 TimeUnit.SECONDS,表示经过 60 秒,多出来的线程还没有接到任务,就会回收,最后保持池子里就 10 个
第五个参数 BlockingQueue 类型,new LinkedBlockingQueue() 用来放任务的集合
*/
execute() 方法添加新任务
public class TestThread
{
public static void main(String[] args) throws InterruptedException
{
ThreadPoolExecutor threadPool= new ThreadPoolExecutor(10, 15, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
threadPool.execute(new Runnable()
{//添加任务
public void run()
{
System.out.println("执行任务");
}
});
}
}
java 中几种线程池
java 线程池的顶级接口是 Executor ,子接口是 ExecutorService ,子接口使用更广泛
Executors 类提供了一系列工厂方法用于创建线程池,返回的线程池实现了 ExecutorService 接口
ExecutorService threadPool = null;
threadPool = Executors.newCachedThreadPool();//缓冲线程池
threadPool = Executors.newFixedThreadPool(3);//固定大小的线程池
threadPool = Executors.newScheduledThreadPool(2);//定时任务线程池
threadPool = Executors.newSingleThreadExecutor();//单线程的线程池
threadPool = new ThreadPoolExecutor(···);//默认线程池,多个可控参数