1. 基本概念
1.1 程序、进程、线程、多线程
- 程序(program)
- 是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
- 进程(process)
- 是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程,有它自身的产生、存在和消亡的过程,即生命周期。
- 进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域,即堆、方法区。
- 线程(thread)
- 进程可进一步细化为线程,是一个程序内部的一条执行路径。
- 若一个进程同一时间并行执行多个线程,就是支持多线程的。
- 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器 pc,线程切换的开销小。
- 一个进程中的多个线程共享这个进程的堆和方法区资源,但每个线程拥有自己独立的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小。
- 一个进程中的多个线程共享相同的内存单元/内存地址空间,它们从同一堆中分配对象,可以访问相同的变量和对象,这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。
- 单核CPU和多核CPU
- 单核CPU,假的多线程,因为在一个时间单元内,只能执行一个线程的任务。
- 多核CPU,才能更好的发挥多线程的效率。现在的服务器都是多核的。
- 一个Java应用程序java.exe,至少有三个线程:main()主线程,gc()垃圾回收线程,异常处理线程。如果发生异常,会影响主线程。
- 并行与并发
- 并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。
- 并发:一个CPU(采用时间片)同时执行多个任务。比如:秒杀,多个人做同一件事。
- 使用多线程的优点
- 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
- 提高计算机系统CPU的利用率。
- 改善程序结构,将既长又复杂的进程分为多个线程,独立运行,利于理解和修改。
何时需要多线程
JVM允许程序运行多个线程,它通过java.lang.Thread类来体现
- Thread类的特性
- 每个线程都是通过某个特定Thread对象的run()方法来完成操作的,把run()方法的主体称为线程执行体
- 通过该Thread对象的start()方法来启动这个线程,而非直接调用run()
- Thread类的构造器
- Thread():创建新的Thread对象,即创建一个线程
- Thread(String name):创建一个线程并指定线程实例名
- Thread(Runnable target):创建一个线程并指定线程的目标对象,它实现了Runnable接口中的run方法
- Thread(Runnable target, String name):创建一个线程并指定线程实例名和线程的目标对象。参数 name为线程名,参数 target为包含线程体的目标对象。
- Thread类的常用静态方法:
- static Thread currentThread(): 静态方法,返回当前正在执行的线程。在Thread子类中就是this,通常用于主线程和Runnable实现类
- interrupted():返回当前执行的线程是否已经被中断
- static void sleep(long millis): 静态方法,使当前执行的线程睡眠多少毫秒数。令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队(进入就绪状态)。即让当前线程睡眠指定的毫秒时间,在指定的时间内,当前线程是阻塞状态。抛出InterruptedException异常
- 区分:object.wai()、object.notify()、object.notifyAll():Object类提供的线程等待和线程唤醒方法
- static void yield(): 线程让步,自愿释放当前cpu的执行权。暂停当前正在执行的线程,把CPU执行机会让给优先级相同或更高的线程。若队列中没有同优先级的线程,忽略此方法
- Thread类的常用实例方法:
- void start(): 启动当前线程,并调用当前线程的的run()方法
- run(): 线程在被调度时执行的操作。通常需要重写Thread类中的run()方法,将创建的线程要执行的操作声明在run()方法中
- getId():返回当前线程的id
- String getName(): 返回当前线程的名称
- void setName(String name): 设置当前线程名称
- join(): 在线程a中调用线程b的join()方法,此时线程a就进入阻塞状态,需要等待直到线程b完全执行完以后,线程a才结束阻塞状态。低优先级的线程也可以获得执行
- join(long millis):等待线程b终止,最多等待多少毫秒数
- stop(): 强制当前线程生命期结束。此方法已过时,不推荐使用
- boolean isAlive(): 判断当前线程是否还处于活动状态
- getPriority(): 返回当前线程优先级等级
- setPriority(int newPriority): 设置当前线程的优先级等级
- interrupt():使该线程中断;
- isInterrupted():返回该线程是否被中断
- isDaemon():返回该线程是否是守护线程
- setDaemon(boolean on):将该线程标记为守护线程或用户线程,如果不标记默认是非守护线程
- 线程的调度
- 调度策略:
- 时间片;
- 抢占式:高优先级的线程抢占CPU
- Java的调度方法:
- 同优先级线程组成先进先出队列(先到先服务),使用时间片策略;
- 对高优先级,使用优先调度的抢占式策略
- 调度策略:
- 线程的优先级
- 线程的优先级等级:MAX_PRIORITY=10、MIN _PRIORITY=1、NORM_PRIORITY=5(默认的优先级)
- 线程创建时继承父线程的优先级
- 低优先级的线程只是获得调度的概率低,并非一定是在高优先级的线程执行完之后才被调用
线程的分类
步骤:
1) 定义子类,继承Thread类;
2) 子类中重写Thread类中的run()方法,将线程需要执行的操作声明在run()中;
3) 创建Thread类的子类的对象,即创建线程对象;
4) 调用线程对象的start()方法,start()方法执行了两步:①启动当前线程 ②调用当前线程的run()方法。
- 注意点:
- 要启动多线程,必须调用start()方法。如果自己手动调用run()方法,那只是普通方法,并没有启动多线程模式。
- run()方法由JVM调用,什么时候调用,执行的过程控制都由操作系统的CPU调度决定。
- 一个线程对象只能调用一次start()方法启动,如果重复调用,将抛出IllegalThreadStateException异常。即不能让已经start()的线程再去执行start()。
为什么调用start()方法时会执行run()方法,为什么不能直接调用run()方法?
- new一个Thread,线程进入了新建状态。调用start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行。start()会执行线程的相应准备工作,然后自动执行run()方法中的内容,这是真正的多线程工作。 但是,直接执行run()方法,会把run()方法当成一个main线程下的普通方法去执行,并不会以多线程的方式去运行。
class MyThread extends Thread {//1. 创建一个继承于Thread类的子类
@Override
public void run() {//2. 子类中重写Thread类的run()
for (int i = 0; i < 100; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
public class ThreadTest {
public static void main(String[] args) {
//子线程1
MyThread t1 = new MyThread();//3. 创建Thread类的子类的对象
t1.start();//4.通过此对象调用start():①启动当前线程 ② 调用当前线程的run()
//t1.run();//不能通过直接调用run()的方式启动线程。
//t1.start();/不能让已经执行start()的线程再去执行start()
//子线程2
MyThread t2 = new MyThread();//需要重新创建一个线程的对象
t2.start();
//main线程
for (int i = 0; i < 100; i++) {//如下操作仍然是在main线程中执行的
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i + "***********main()************");
}
}
}
}
2.3 创建线程的方式二:实现Runnable接口
- new一个Thread,线程进入了新建状态。调用start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行。start()会执行线程的相应准备工作,然后自动执行run()方法中的内容,这是真正的多线程工作。 但是,直接执行run()方法,会把run()方法当成一个main线程下的普通方法去执行,并不会以多线程的方式去运行。
步骤:
1) 定义实现类,实现Runnable接口;
2) 实现类中实现Runnable接口中的run()方法,将线程需要执行的操作声明在run()中;
3) 创建Runnable接口的实现类的对象,将Runnable接口的实现类的对象作为实际参数传递给Thread类的含参构造器中,创建Thread类的对象,即创建线程对象;
4) 调用线程对象的start()方法,start()方法执行了两步:①启动当前线程 ②调用当前线程的run()方法。
class MThread implements Runnable{//1. 创建一个实现了Runnable接口的类
@Override
public void run() {//2. 实现类去实现Runnable中的run()
for (int i = 0; i < 100; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
public class ThreadTest1 {
public static void main(String[] args) {
MThread mThread = new MThread();//3.创建实现类的对象
Thread t1 = new Thread(mThread);//4.将实现类的对象作为参数传递到Thread类的构造器中,创建Thread类的对象
t1.setName("线程1");
t1.start();//5.通过Thread类的对象调用start()
Thread t2 = new Thread(mThread);//再创建一个线程对象
t2.setName("线程2");
t2.start();
}
}
2.4 创建线程的方式三:实现Callable接口
- 步骤:
1) 定义实现类,实现Callable接口;
2) 实现类中实现Callable接口中的call()方法,将线程需要执行的操作写在call()中,且call()方法有返回值;
3) 创建Callable接口的实现类的对象,将Callable接口的实现类的对象作为实际参数传递给FutureTask类的含参构造器,创建FutureTask类的对象,该FutureTask对象封装了该Callable对象的call()方法的返回值;
4) 将FutureTask类的对象作为实际参数传递给Thread类的含参构造器中,创建Thread类的对象,即创建线程对象;
5) 调用线程对象的start()方法,start()方法执行了两步:①启动当前线程 ②调用当前线程的run()方法。
6) 获取Callable接口的实现类的对象所在实现类中call()方法的返回值:通过调用FutureTask对象的get()方法
- 与实现Runnable接口相比,实现Callable接口创建线程的方式,功能更强大:
- 相比Runnable接口的run()方法,Callable接口的call()方法可以有返回值
- 借助FutureTask类,获取Callable任务的Object类型的返回值:通过FutureTask的对象.get()方法,获取创建此FutureTask对象时,传入FutureTask含参构造器参数的,Callable接口的实现类的对象所在实现类中重写的call()方法的返回值。
- 返回值可以给别的线程使用
- call()方法支持泛型的返回值
- call()方法可以抛出异常,被外面的操作捕获,获取异常的信息
- 相比Runnable接口的run()方法,Callable接口的call()方法可以有返回值
- Future接口:
- 可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。
- FutrueTask是Futrue接口的唯一的实现类
- FutureTask同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
继承方式和实现方式的联系与区别:
- 联系:public class Thread implements Runnbale
- 共同点:
- 两种方式都需要重写run()方法,将线程要执行的逻辑声明在run()方法中
- 要想启动线程,都是调用start()方法
- 区别:
- 继承Thread类,线程代码存放Thread类的子类run方法中。
- 实现Runnable/Runnable接口,线程代码存在接口的实现类的run方法。
- 实现方式的优缺点:
- 优:
- 避免了类的单继承的局限性。线程类只是实现了Runnable或Callable接口,还可以继承其他类;
- 在这种方式下,多个线程可以共享同一个接口实现类的对象(target),所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
- 缺:编程稍稍复杂,如果需要访问当前线程,则必须使用Thread.currentThread()方法。
- 优:
- 继承方式的优缺点:
- 缺:因为线程类已经继承了Thread类,所以不能再继承其他父类。
- 优:编写简单,如果需要访问当前线程,则无须使用Thread.currentThread()方法,直接使用this即可获得当前线程。
- 开发中优先选择:实现接口的方式来创建多线程。
class NumThread implements Callable{//1.创建一个实现Callable的实现类
@Override
public Object call() throws Exception {//2.实现call方法,将此线程需要执行的操作声明在call()中
int sum = 0;
for (int i = 1; i <= 100; i++) {
if(i % 2 == 0){
System.out.println(i);
sum += i;
}
}
return sum;
}
}
public class ThreadNew {
public static void main(String[] args) {
//3.创建Callable接口实现类的对象,将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
NumThread numThread = new NumThread();
FutureTask futureTask = new FutureTask(numThread);
//4.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象
new Thread(futureTask).start();//5.调用线程对象的start()方法
try {
//6.获取Callable接口实现类的对象所在实现类中call()方法的返回值
//FutureTask的对象.get()方法的返回值,即为创建此FutureTask对象时,
//传入FutureTask含参构造器参数的Callable接口实现类对象所在实现类重写的call()的返回值
Object sum = futureTask.get();
System.out.println("总和为:" + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
2.5 创建线程的方式四:使用线程池
具体看6.线程池
- 使用步骤:
1) 提供/创建一个指定线程数量的线程
2) 设置线程池的属性(可省略)
3) 执行指定的线程的操作:需要提供实现Runnable接口或Callable接口的实现类的对象
4) 关闭连接池
public class ThreadPool {
public static void main(String[] args) {
//1. 提供一个指定线程数量的线程池
// 通过Executors.newFixedThreadPool(n); 创建ExecutorService类型的对象
// 再强转成ThreadPoolExecutor类型
ExecutorService service = Executors.newFixedThreadPool(10);
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
//2.设置线程池的属性
// System.out.println(service.getClass());
// service1.setCorePoolSize(15);
// service1.setKeepAliveTime();
//3.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
service.execute(new NumberThread());//适合适用于Runnable
service.execute(new NumberThread1());//适合适用于Runnable
// service.submit(Callable callable);//适合使用于Callable
//4.关闭连接池
service.shutdown();
}
}
class NumberThread implements Runnable{//实现Runnable接口的实现类
@Override
public void run() {
for(int i = 0;i <= 100;i++){
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
class NumberThread1 implements Runnable{//实现Runnable接口的实现类
@Override
public void run() {
for(int i = 0;i <= 100;i++){
if(i % 2 != 0){
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
ExecutorService结合Callable实现有返回结果的多线程:
- 执行Callable 任务后,可以获取一个 Future 的对象,在该对象上调用 get 就可以获取到 Callable 任务返回的 Object 了,再结合线程池接口 ExecutorService 就可以实现有返回结果的多线程。
//创建一个线程池
ExecutorService pool = Executors.newFixedThreadPool(taskSize);
// 创建多个有返回值的任务
List<Future> list = new ArrayList<Future>();
for (int i = 0; i < taskSize; i++) {
Callable c = new MyCallable(i + " ");
// 执行任务并获取 Future 对象
Future f = pool.submit(c);
list.add(f);
}
// 关闭线程池
pool.shutdown();
// 获取所有并发任务的运行结果
for (Future f : list) {
// 从 Future 对象上获取任务的返回值,并输出到控制台
System.out.println("res:" + f.get().toString());
}
3. 线程的生命周期
- 执行Callable 任务后,可以获取一个 Future 的对象,在该对象上调用 get 就可以获取到 Callable 任务返回的 Object 了,再结合线程池接口 ExecutorService 就可以实现有返回结果的多线程。
JDK中用Thread.State类定义了线程在一个完整的生命周期中通常要经历的几种状态:
- 新建
NEW
:新创建的线程对象,还没有调用start()方法启动,也称为初始状态、开始状态。 - 就绪
RUNNABLE
:处于新建状态的线程被start()后,将进入线程队列,等待CPU的使用权,此时它已具备了运行的条件,只是没分配到CPU资源,也称为就绪状态、可运行状态。- 调用start()方法后线程进入就绪状态,并不是说只要调用start()方法线程就马上变为当前线程,在变为当前线程之前都是就绪状态。
- 线程在睡眠或挂起恢复时,即sleep()超时,或join()等待线程终止或超时,或获取同步锁,或I/O操作完毕,或调用notify()/notifyAll()线程唤醒方法,线程也会进入就绪状态。
- 调用yield()方法,线程让步,释放线程的CPU执行权,暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程,线程也会进入就绪状态。
- 运行 RUNNING:当就绪的线程被调度并获得CPU资源时,便进入运行状态,开始执行run()方法,run()方法定义了线程的操作和功能。
- 阻塞
BLOCKED
:由于某种原因,让出CPU使用权并临时中止自己的执行,进入阻塞状态- 等待阻塞:通过调用线程的Object.wait()方法,JVM会将线程放进入等待队列,让线程等待某工作的完成。
- 同步阻塞:线程在获取同步锁时,同步锁被别的线程占用,获取同步锁失败,JVM会将线程放入锁池中。线程因为等待监视锁而被阻塞,进入同步阻塞状态。
- 其他阻塞:通过调用线程的sleep(),或调用其他线程的join(),或发出了I/O操作请求,线程会进入阻塞。当sleep()超时,或join()等待线程终止或超时,或I/O完毕,线程就重新进入就绪状态。
- 等待
WAITING
:- 造成线程等待的原因有三种,分别是调用Object.wait()、Thread.join()以及LockSupport.park()方法。
- 处于等待状态的线程,正在等待其他线程去执行一个特定的操作。例如:因为Object.wait()而等待的线程正在等待另一个线程去调用Object.notify()或Object.notifyAll();因为Thread.join()而等待的线程正在等待另一个线程结束。
- 限时等待
TIMED_WAITING
:一个在限定时间内等待的线程的状态,也称为限时等待状态。造成线程限时等待状态的原因有五种,分别是:Thread.sleep(long)、Object.wait(long)、Thread.join(long)、LockSupport.parkNanos(obj,long)、LockSupport.parkUntil(obj,long)。 - 终止
TERMINATED
:一个完全运行完成的线程的状态,也称为终止状态、结束状态。 - 死亡 DEAD:线程完成了它的全部工作,或线程被提前强制性地中止或出现异常导致结束。
- 正常结束:run()或call()方法执行完成,线程正常结束。
- 异常结束:线程抛出一个未捕获的 Exception 或 Error。
- 调用 stop():直接调用该线程的stop()方法来结束该线程。该方法通常容易导致死锁,不推荐使用。
- 新建
4. 线程的同步
4.1 多线程安全问题
- 多线程出现安全问题的原因:当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行,导致共享数据的错误。即,多个线程执行的不确定性引起执行结果的不稳定,会造成操作的不完整性,破坏数据。
解决办法:对多条操作共享数据的语句,只让一个线程都执行完,在执行过程中其他线程不可以参与执行。
4.2 同步机制 synchronized
同步机制
- 在Java中,通过同步机制synchronized,来解决多线程的安全问题。
- 好处:同步的方式解决了线程的安全问题。
- 局限性:只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。
- 同步机制中的锁
- 同步锁机制:对于并发工作,你需要某种方式来防止两个任务访问相同的资源(其实就是共享资源竞争)。防止这种冲突的方法就是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须锁定这项资源,使其他的任务在其被解锁之前,就无法访问资源,而在其被解锁之时,另一个任务就可以锁定并使用资源了。
- 同步机制中的锁是什么:同步锁、监视器、同步监视器
- 任何一个类的对象都可以作为同步锁。所有对象都自动含有单一的锁,即监视器。
- 同步代码块的锁:自己指定,很多时候指定为this或类名.class
- 同步方法的锁:静态方法:类名.class;非静态方法:this
- 注意:
- 要求:必须确保使用同一个资源的多个线程必须共用同一把锁,否则无法保证共享资源的安全
- 一个线程类中的所有静态方法共用同一把锁:类名.class;所有非静态方法共用同一把锁:this;同步代码块:指定同步锁时需谨慎
- 同步的范围
- 如何找问题,即代码是否存在线程安全?
- 明确哪些代码是多线程运行的代码
- 明确多个线程是否有共享数据
- 明确多线程运行代码中是否有多条语句操作共享数据
- 如何解决呢?
- 对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。即所有操作共享数据的这些语句都要放在同步范围中。
- 同步的范围:操作共享数据(多个线程共同操作的变量)的代码,即为需要被同步的代码。不能包含代码多了,也不能包含少了。
- 范围太小:没锁住所有有安全问题的代码。
- 范围太大:没发挥多线程的功能。
- 如何找问题,即代码是否存在线程安全?
- 释放锁的操作
- 当前线程的同步代码块、同步方法执行结束。
- 当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。
- 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束。
- 当前线程在同步代码块、同步方法中执行了线程对象的Object.wait()方法,当前线程暂停,并释放锁。
- 不会释放锁的操作
- 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行。
- 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁。
- 应尽量避免使用suspend()和resume()来控制线程
线程的死锁问题
使用同步代码块解决实现Runnable接口的方式创建多线程的线程安全问题中,可以考虑使用
this
充当同步监视器。- 使用同步代码块解决继承Thread类的方式创建多线程的线程安全问题中,慎用this充当同步监视器,可以考虑使用
当前类.class
充当同步监视器。
```java class Window1 implements Runnable{ private int ticket = 100; // Object obj = new Object(); // Dog dog = new Dog(); @Override public void run() { // Object obj = new Object();synchronized (对象){//此处传入的对象就是同步锁、同步监视器
// 需要被同步的代码;
}
} }while(true){
synchronized (this){//此时的this:唯一的Window1的对象 //方式二:synchronized (dog) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
} else {
break;
}
}
}
public class WindowTest1 { public static void main(String[] args) { Window1 w = new Window1(); Thread t1 = new Thread(w); Thread t2 = new Thread(w); Thread t3 = new Thread(w); t1.setName(“窗口1”); t2.setName(“窗口2”); t3.setName(“窗口3”); t1.start(); t2.start(); t3.start(); } }
class Dog{ }
```java
class Window2 extends Thread{
private static int ticket = 100;
private static Object obj = new Object();
@Override
public void run() {
while(true){
//正确的
// synchronized (obj){
synchronized (Window2.class){//Class clazz = Window2.class,Window2.class只会加载一次
//错误的方式:this代表着t1,t2,t3三个对象
// synchronized (this){
if(ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + ":卖票,票号为:" + ticket);
ticket--;
}else{
break;
}
}
}
}
}
public class WindowTest2 {
public static void main(String[] args) {
Window2 t1 = new Window2();
Window2 t2 = new Window2();
Window2 t3 = new Window2();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
4.3 解决线程安全问题的方式二:同步方法
- synchronized还可以放在方法声明中,表示整个方法为同步方法。
- 同步方法仍然涉及到同步监视器,只是不需要我们显示的声明。
- 非静态的同步方法,同步监视器是:this。
静态的同步方法,同步监视器是:当前类本身。所以这时特别注意当处理继承Thread类的线程安全问题时,同步监视器不能是this,而应该是当前类,所以此时要把同步方法改为静态的。
public synchronized void show (String name){
…
}
```java class Window3 implements Runnable {
private int ticket = 100;
@Override public void run() {
while (true) {
show();
}
}
private synchronized void show(){//同步监视器:this
//synchronized (this){
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
}
//}
} }
public class WindowTest3 { public static void main(String[] args) { Window3 w = new Window3();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
```java
class Bank{
private Bank(){}
private static Bank instance = null;
public static Bank getInstance(){
//方式一:效率稍差
// synchronized (Bank.class) {
// if(instance == null){
// instance = new Bank();
// }
// return instance;
// }
//方式二:效率更高
if(instance == null){
synchronized (Bank.class) {
if(instance == null){
instance = new Bank();
}
}
}
return instance;
}
}
public class BankTest {
Bank b1=Bank.getInstance();
Bank b2=Bank.getInstance();
System.out.println(b1==b2);
}
4.4 解决线程安全问题的方式三:Lock锁同步锁
- 从JDK 5.0开始,Java提供了更强大的线程同步机制:通过显式定义同步锁对象来实现同步,同步锁使用Lock对象充当。
- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
ReentrantLock类实现了Lock接口,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
class A{
private final ReentrantLock lock = new ReenTrantLock();
public void m(){
lock.lock();
try{
//保证线程安全的代码;
}
finally{
lock.unlock(); //注意:如果同步代码有异常,要将unlock()写入finally语句块
}
}
}
class Window implements Runnable{
private int ticket = 100;
//1.实例化ReentrantLock
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while(true){
try{
//2.调用锁定方法lock()
lock.lock();
if(ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);
ticket--;
}else{
break;
}
}finally {
//3.调用解锁方法:unlock()
lock.unlock();
}
}
}
}
public class LockTest {
public static void main(String[] args) {
Window w = new Window();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
synchronized 与 Lock 的对比
- Lock是显式锁,需要手动开启和关闭锁;synchronized是隐式锁,出了作用域自动释放。
- Lock只有代码块锁,synchronized有代码块锁和方法锁。
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
优先使用顺序:
wait():令当前线程挂起并放弃CPU、同步资源并等待,使别的线程可访问并修改共享资源,而当前线程排队等候其他线程调用notify()或notifyAll()方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行。
- 在当前线程中调用方法:对象名.wait()
- 使当前线程进入等待(某对象)状态,直到另一线程对该对象发出notify或notifyAll为止。
- 调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)
- 调用此方法后,当前线程将释放对象监控权,然后进入等待
- 在当前线程被notify()或notifyAll()方法唤醒后,要重新获得监控权,然后从断点处继续代码的执行。
- notify()/notifyAll():唤醒正在排队等待同步资源的线程中优先级最高者结束等待 / 唤醒正在排队等待资源的所有线程结束等待。
- 在当前线程中调用方法:对象名.notify()/notifyAll()
- 功能:唤醒等待该对象监控权的一个/所有线程。
- 调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)
- 这三个方法只有在synchronized方法或代码块中或加锁才能使用,否则会报IllegalMonitorStateException异常。
因为这三个方法必须有锁对象调用,而任意对象都可以作为synchronized的同步锁,因此这三个方法只能在Object类中声明。 ```java /**
- 线程通信的应用:经典例题:生产者/消费者问题 *
- 生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,
- 店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员
- 会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品
- 了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
- 可能出现两个问题:
- 1.生产者比消费者快时,消费者会漏掉一些数据没有取到。
- 2.消费者比生产者快时,消费者会取相同的数据。
- 分析:
- 是否是多线程问题?是,生产者线程,消费者线程
- 是否有共享数据?是,店员(或产品)
- 如何解决线程的安全问题?同步机制,有三种方法
- 是否涉及线程的通信?是 */ class Clerk{
private int productCount = 0; //生产产品 public synchronized void produceProduct() {
if(productCount < 20){
productCount++;
System.out.println(Thread.currentThread().getName() + ":开始生产第" + productCount + "个产品");
notify();
}else{
//等待
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} //消费产品 public synchronized void consumeProduct() {
if(productCount > 0){
System.out.println(Thread.currentThread().getName() + ":开始消费第" + productCount + "个产品");
productCount--;
notify();
}else{
//等待
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} }
class Producer extends Thread{//生产者
private Clerk clerk;
public Producer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(getName() + ":开始生产产品.....");
while(true){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.produceProduct();
}
}
}
class Consumer extends Thread{//消费者 private Clerk clerk;
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(getName() + ":开始消费产品.....");
while(true){
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.consumeProduct();
}
}
}
public class ProductTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Producer p1 = new Producer(clerk);
p1.setName("生产者1");
Consumer c1 = new Consumer(clerk);
c1.setName("消费者1");
Consumer c2 = new Consumer(clerk);
c2.setName("消费者2");
p1.start();
c1.start();
c2.start();
}
}
<a name="Fty2T"></a>
# 6. 线程池
- 背景:
- 经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
- 思路:
- 提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。
- 好处:
- 提高响应速度:减少了创建新线程的时间。当任务到达时,任务可以不需要等到线程创建就能立即执行
- 降低资源消耗:重复利用线程池中已创建的线程。不需要每次都创建,降低线程创建和销毁造成的消耗
- 便于线程管理:设置线程池的属性。提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
<a name="ZAgzR"></a>
## 6.1 线程池的使用:**ThreadPoolExecutor实现类**
- **线程池相关API:**
- JDK 5.0起提供了线程池相关API:ExecutorService 和 Executors
- **ExecutorService接口:**
- 真正的线程池接口,JDK普通线程池。常见实现类ThreadPoolExecutor
- void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable接口的实现类对象
- <T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般用来执行Callable接口的实现类对象
- void shutdown():关闭连接池
- **ThreadPoolExecutor实现类:**是ExecutorService的常见实现类、线程池的真正实现类。
```java
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
- 线程池的属性/参数:
- corePoolSize(必需):核心线程数(核心池的大小)。默认情况下,核心线程会一直存活,但是当将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
- maximumPoolSize(必需):线程池所能容纳的最大线程数。当活跃线程数达到该数值后,后续的新任务将会阻塞。
- keepAliveTime(必需):线程闲置超时时长。如果超过该时长,非核心线程就会被回收(线程没有任务时最多保持多长时间后会终止)。如果将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
- unit(必需):指定 keepAliveTime 参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。
- workQueue(必需):任务队列。通过线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中。其采用阻塞队列实现。
- threadFactory(可选):线程工厂。用于指定为线程池创建新线程的方式。
- handler(可选):拒绝策略。当达到最大线程数时需要执行的饱和策略
- 设置线程池的属性
- setCorePoolSize(n)
- setMaximumPoolSize
- setKeepAliveTime(n)
- ….
- 线程池的使用流程如下:
// 创建线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(CORE_POOL_SIZE,
MAXIMUM_POOL_SIZE,
KEEP_ALIVE,
TimeUnit.SECONDS,
sPoolWorkQueue,
sThreadFactory);
// 向线程池提交任务
threadPool.execute(new Runnable() {
@Override
public void run() {
... // 线程执行的任务
}
});
// 关闭线程池
threadPool.shutdown(); // 设置线程池的状态为SHUTDOWN,然后中断所有没有正在执行任务的线程
threadPool.shutdownNow(); // 设置线程池的状态为 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表
6.2 线程池工作过程
6.3 任务队列/阻塞队列(workQueue)
任务队列是基于阻塞队列实现的,即采用生产者消费者模式,在 Java 中需要实现 BlockingQueue 接口。但 Java 已经为我们提供了 7 种阻塞队列的实现:
- ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列(数组结构可配合指针实现一个环形队列)。
- LinkedBlockingQueue: 一个由链表结构组成的可有界可无界的阻塞队列,在未指明容量时,容量默认为 Integer.MAX_VALUE。
- PriorityBlockingQueue: 一个支持优先级排序的无界阻塞队列,对元素没有要求,可以实现 Comparable 接口也可以提供 Comparator 来对队列中的元素进行比较。跟时间没有任何关系,仅仅是按照优先级取任务。
- DelayQueue:类似于PriorityBlockingQueue,是二叉堆实现的无界优先级阻塞队列。要求元素都实现 Delayed 接口,通过执行时延从队列中提取任务,时间没到任务取不出来。
- SynchronousQueue: 一个不存储元素的阻塞队列,消费者线程调用 take() 方法的时候就会发生阻塞,直到有一个生产者线程生产了一个元素,消费者线程就可以拿到这个元素并返回;生产者线程调用 put() 方法的时候也会发生阻塞,直到有一个消费者线程消费了一个元素,生产者才会返回。
- LinkedTransferQueue: 它是ConcurrentLinkedQueue、LinkedBlockingQueue 和 SynchronousQueue 的结合体,但是把它用在 ThreadPoolExecutor 中,和 LinkedBlockingQueue 行为一致,都是无界的阻塞队列。
- LinkedBlockingDeque: 使用双向队列实现的有界双端阻塞队列。双端意味着可以像普通队列一样 FIFO(先进先出),也可以像栈一样 FILO(先进后出)。
注意有界队列和无界队列的区别:
如果使用有界队列,当队列饱和时并超过最大线程数时就会执行拒绝策略;
如果使用无界队列,因为任务队列永远都可以添加任务,直到系统资源耗尽。
ArrayBlockingQueue、LinkedBlockingQueue、ConcurrentLinkedQueue的区别
ArrayBlockingQueue | LinkedBlockingQueue | ConcurrentLinkedQueue | |
---|---|---|---|
阻塞与否 | 阻塞 | 阻塞 | 非阻塞 |
是否有界 | 有界,适合已知最大存储容量的场景 | 可配置,可有界可以无界 | 无界 |
线程安全保障 (并发方面) |
一把全局锁(采用一把锁,两个condition)(还支持公平锁) | 存取采用2把锁(头尾各1把锁) | CAS |
适用场景 | 生产消费模型,平衡两边处理速度 | 生产消费模型,平衡两边处理速度 | 对全局的集合进行操作的场景 |
注意事项 | 用于存储队列元素的存储空间是预先分配的,使用过程中内存开销较小(无须动态申请存储空间) | 无界的时候注意内存溢出问题,用于存储队列元素的存储空间是在其使用过程中动态分配的,因此它可能会增加JVM垃圾回收的负担。 | size() 是要遍历一遍集合,慎用 |
内存方面 | 用于存储队列元素的存储空间是预先分配的,使用过程中内存开销较小(无须动态申请存储空间) | 用于存储队列元素的存储空间是在其使用过程中动态分配的,因此它可能会增加JVM垃圾回收的负担。 | |
吞吐量 | LinkedBlockingQueue在大多数并发的场景下吞吐量比ArrayBlockingQueue高,但是性能不稳定。LinkedBlockingQueue 之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步。 |
6.4 线程工厂(threadFactory)
线程工厂指定创建线程的方式,需要实现 ThreadFactory 接口,并实现 newThread(Runnable r) 方法。该参数可以不用指定,Executors 框架已经为我们实现了一个默认的线程工厂:DefaultThreadFactory (详细看源码)
6.5 拒绝策略(handler)
当线程池的线程数达到最大线程数时,需要执行拒绝策略。拒绝策略需要实现 RejectedExecutionHandler 接口,并实现 rejectedExecution(Runnable r, ThreadPoolExecutor executor) 方法。不过 Executors 框架已经为我们实现了 4 种拒绝策略:
- AbortPolicy(默认):丢弃任务并抛出 RejectedExecutionException 异常。
- CallerRunsPolicy:由调用线程处理该任务。
- DiscardPolicy:丢弃任务,但是不抛出异常。可以配合这种模式进行自定义的处理方式。
- DiscardOldestPolicy:丢弃队列最早的未处理任务,然后重新尝试执行任务。
6.6 四种常见的功能线程池
- Executors工具类:
- 工具类、线程池的工厂类,用于创建并返回不同类型的线程池。
- ThreadPoolExecutor是Executors类的底层实现
- 四种线程池:
- 可缓存线程池CachedThreadPool:Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池。
- 定长线程池 FixedThreadPool:Executors.newFixedThreadPool(n):创建一个可重用固定线程数的线程池。
- 单线程化线程池 SingleThreadExecutor:Executors.newSingleThreadExecutor():创建一个只有一个线程的线程池。
- 定时线程池 ScheduledThreadPool :Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
定长线程池(FixedThreadPool)
- 特点:只有核心线程,线程数量固定,执行完立即回收,任务队列为链表结构的有界队列LinkedBlockingQueue。
- 应用场景:控制线程最大并发数。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
// 1. 创建定长线程池对象 & 设置线程池线程数量固定为3
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
public void run() {
System.out.println("执行任务啦");
}
};
// 3. 向线程池提交任务
fixedThreadPool.execute(task);
定时线程池(ScheduledThreadPool )
- 特点:核心线程数量固定,非核心线程数量无限,执行完闲置 10ms 后回收,任务队列为延时阻塞队列DelayedWorkQueue。
- 应用场景:执行定时或周期性的任务 ```java private static final long DEFAULT_KEEPALIVE_MILLIS = 10L;
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { return new ScheduledThreadPoolExecutor(corePoolSize); } public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS, new DelayedWorkQueue()); }
public static ScheduledExecutorService newScheduledThreadPool( int corePoolSize, ThreadFactory threadFactory) { return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory); } public ScheduledThreadPoolExecutor(int corePoolSize, ThreadFactory threadFactory) { super(corePoolSize, Integer.MAX_VALUE, DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS, new DelayedWorkQueue(), threadFactory); }
```java
// 1. 创建 定时线程池对象 & 设置线程池线程数量固定为5
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
public void run() {
System.out.println("执行任务啦");
}
};
// 3. 向线程池提交任务
scheduledThreadPool.schedule(task, 1, TimeUnit.SECONDS); // 延迟1s后执行任务
scheduledThreadPool.scheduleAtFixedRate(task,10,1000,TimeUnit.MILLISECONDS);// 延迟10ms后、每隔1000ms执行任务
可缓存线程池(CachedThreadPool)
- 特点:无核心线程,非核心线程数量无限,执行完闲置 60s 后回收,任务队列为不存储元素的阻塞队列 SynchronousQueue。
- 应用场景:执行大量、耗时少的任务。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
// 1. 创建可缓存线程池对象
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
public void run() {
System.out.println("执行任务啦");
}
};
// 3. 向线程池提交任务
cachedThreadPool.execute(task);
单线程化线程池(SingleThreadExecutor)
- 特点:只有 1 个核心线程,无非核心线程,执行完立即回收,任务队列为链表结构的有界队列LinkedBlockingQueue。
- 应用场景:不适合并发但可能引起 IO 阻塞性及影响 UI 线程响应的操作,如数据库操作、文件操作等。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
// 1. 创建单线程化线程池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
public void run() {
System.out.println("执行任务啦");
}
};
// 3. 向线程池提交任务
singleThreadExecutor.execute(task);
对比
- 总结
Executors 的 4 个功能线程池虽然方便,但现在已经不建议使用了,而是建议直接通过使用 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
其实 Executors 的 4 个功能线程有如下弊端:
FixedThreadPool 和 SingleThreadExecutor:主要问题是堆积的请求处理队列均采用 LinkedBlockingQueue,可能会耗费非常大的内存,甚至 OOM。
CachedThreadPool 和 ScheduledThreadPool:主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。
7. 常用的辅助类
CountDownLatch 减法计数器
允许一个或多个线程等待直到在其他线程中执行的一组操作完成的同步辅助。
原理:每次有线程调用 countDown(),计数器的计数-1,直到计数变为0,await() 就会被唤醒,继续执行之后的操作。
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
// 总数是6,必须要执行任务的时候,再使用
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 1; i <=6 ; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" Go out");
countDownLatch.countDown(); // countDown() 计数-1
},String.valueOf(i)).start();
}
countDownLatch.await(); // await() 等待计数器归零,就唤醒,再继续向下执行
System.out.println("Close Door");
}
}
CyclicBarrier 加法计数器
允许一组线程全部等待彼此达到共同屏障点的同步辅助。
public class CyclicBarrierDemo {
public static void main(String[] args) {
//主线程:召唤龙珠的线程
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
System.out.println("召唤神龙成功!");
});
for (int i = 1; i <= 7; i++) {
//子线程
final int temp = i;
// Lambda表达式(匿名类)不能访问非final的局部变量:
// 因为实例变量存在堆中,而局部变量是在栈上分配,
// Lambda表达((匿名类)会在另一个线程中执行。
// 如果在线程中要直接访问一个局部变量,可能线程执行时该局部变量已经被销毁了,
// 而 final 类型的局部变量在 Lambda 表达式(匿名类) 中其实是局部变量的一个拷贝
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" 收集了第 {"+ finalI+"} 颗龙珠");
try {
cyclicBarrier.await(); //加法计数,等待
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
Semaphore 信号量
原理:一个计数信号量。 在概念上,信号量维持一组许可证。
- 通过 acquire() 获取一个许可,如果没有就等待阻塞,直到有许可证可用
- 每个 release() 添加/释放一个许可证,潜在地释放阻塞获取方
- 方法:
- acquire():获得资源,如果资源已经使用完了,就等待资源释放后再进行使用
- release():释放,会将当前的信号量释放+1,然后唤醒等待的线程
作用: 多个共享资源互斥的使用! 并发限流,控制最大的线程数!
public class SemaphoreDemo {
public static void main(String[] args) {
// 线程数量:停车位! 限流!
Semaphore semaphore = new Semaphore(3);
for (int i = 1; i <= 6; i++) {
final int temp = i; //如果Lambda表达式(匿名类)中没有使用这个局部变量i,就不需要先转成final
new Thread(()->{
try {
semaphore.acquire(); //acquire()得到
System.out.println(Thread.currentThread().getName() + ":第" + temp +"辆车占用到了一个车位");
//System.out.println(Thread.currentThread().getName()+" 抢到了车位");
TimeUnit.SECONDS.sleep(2); //停车2s
System.out.println(Thread.currentThread().getName() + ":第" + temp +"辆车离开了一个车位");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semaphore.release();//release()释放
}
},String.valueOf(i)).start();
}
}
}
8. JMM
volatile关键字
volatile是 Java 虚拟机提供轻量级的同步机制。
- Volatile 是可以保持可见性,不能保证原子性,由于内存屏障,可以保证避免指令重排的现象产生。
1、保证可见性 确保将变量的更新操作通知到其他线程:在读取 volatile 类型的变量时总会返回最新写入的值。
public class JMMDemo01 {
// 如果不加 volatile 程序会死循环
// 加了 volatile 可以保证可见性(子线程对主内存变化的可变性)
private volatile static Integer number = 0;
public static void main(String[] args) {
// main线程
// 子线程1
new Thread(()->{
while (number==0){// 子线程1对主内存的变化不知道的
}
}).start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
number = 1;
System.out.println(number);
}
}
2、不保证原子性
原子性 : 不可分割。线程A在执行任务的时候,不能被打扰的,也不能被分割。要么同时成功,要么同时失败
public class VDemo02 {
// 加了 volatile 可以保证可见性,但不保证原子性
private static volatile int number = 0;
public static void add(){
number++; //++ 自增,不是一个原子性操作,是2个~3个操作
}
public static void main(String[] args) {
//理论上number==20000,但实际不是
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 1; j <= 1000 ; j++) {
add();
}
}).start();
}
while (Thread.activeCount()>2){// main gc
// Thread.yield()是在主线程中执行的,意思只要还有除了gc和main线程之外的线程在跑
// 主线程就让出cpu不往下执行,导致主线程中的子线程还没有执行完,主线程就停止了。
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+",num="+number);
}
}
如果不加lock和synchronized ,怎么样保证原子性?
使用JUC下原子包java.util.concurrent.atomic下的原子类,解决原子性问题。
- 这些类的底层都直接和操作系统挂钩!在内存中修改值!
- 底层使用了Unsafe类,Unsafe类是一个很特殊的存在!
public class VDemo02 {
// 加了 volatile 可以保证可见性,但不保证原子性
// 原子类,解决原子性问题
private volatile static AtomicInteger num = new AtomicInteger();
public static void add(){
// num++; // 不是一个原子性操作
num.getAndIncrement(); // AtomicInteger + 1 方法,底层是CAS保证的原子性
}
public static void main(String[] args) {
//理论和实际上 num==2 万
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000 ; j++) {
add();
}
}).start();
}
while (Thread.activeCount()>2){ // main gc
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + " " + num);
}
}
3、禁止指令重排
- 我们写的程序,计算机并不是按照我们自己写的那样去执行的
- 源代码–>编译器优化重排–>指令并行也可能会重排–>内存系统也会重排–>执行
- 处理器在进行指令重排的时候,会考虑数据之间的依赖性!
- 内存屏障。CPU指令。作用:
- 保证特定的操作的执行顺序!
- 保证某些变量的内存可见性 (利用这些特性volatile实现了可见性)
JMM
- JMM:Java内存模型。不存在的东西,概念!约定!!
- 关于JMM的一些同步的约定:
1、线程解锁前,必须把共享变量立刻刷回主存。
2、线程加锁前,必须读取主存中的最新值到工作内存中!
3、加锁和解锁是同一把锁。