线程 Thread
- 进程与线程
- 进程:进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
- 线程:线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
- 线程的实现
继承Thread类
1 Thread t = new T1(); 2 t.start(); 3 Thread t1 = new T1(); 4 t1.start(); 5 System.out.println(Thread.currentThread().getName());
join() 和 yield()
join:A.join()等待A线程,让A先执行
yield:礼让
1 //Thread类 2 //继承 3 class T1 extends Thread{ 4 5 Thread t; 6 @Override 7 public void run() { 8 // 线程里面需要执行的内容 9 for(int i = 1 ; i <= 100 ; i++) { 10 if( i == 50 ) { 11 try { 12 if(t!=null) { 13 t.join(1);//join是插队,里面的数字代表插队多少毫秒,过了这个时间之后就继续抢占 14 } 15 } catch (InterruptedException e) { 16 // TODO Auto-generated catch block 17 e.printStackTrace(); 18 } 19 Thread.yield();//如果执行到50,就礼让一下 20 } 21 System.out.println(Thread.currentThread().getName() + "----------" + i); 22 } 23 } 24 }
yeild的主函数
1 T1 t1 = new T1();
2 T1 t2 = new T1();
3 t1.t = t2;
4 t2.t = null;
5 t1.start();
6 t2.start();
实现Runnable接口
Runnable和Thread的区别
1 //Runnable和Thread的区别就是Runnable里的资源可以让别人共享 2 //r1线程的资源可以向两条线程里面共享 3 Runnable r1 = new R1(); 4 Thread t1 = new Thread(r1); 5 Thread t2 = new Thread(r1); 6 t1.start(); 7 t2.start(); 8 Thread.sleep(10);//main函数调用别的线程的时候,自己不会停止,有可能另一个线程还没执行完,输出会什么都没有 9 System.out.println(((R1)r1).strB);
1 class R1 implements Runnable{ 2 StringBuilder strB = new StringBuilder(); 3 @Override 4 public void run() { 5 for(int i = 1 ; i < 10 ; i++) { 6 strB.append(i); 7 //在这里不可以throws只能try catch,因为Runnable里面的run没有throws 8 //只能更精确,不能更宽泛 9 try { 10 Thread.sleep(1); 11 } catch (InterruptedException e) { 12 e.printStackTrace(); 13 } 14 } 15 } 16 }
平常使用Runnable最好,Runnable里面的成员可以被多个线程共享
Runnable方法没有返回值,实现的是run()方法
Callable方法重写了call()方法,有返回值,可以抛出异常.
- 线程的常用方法
- 线程的生命周期
线程的五状态图
包括 : 新建(new) 就绪(runnable) 运行(running) 堵塞(blocked) 死亡(dead)
- 线程安全
线程安全就是说多线程访问同一代码,不会产生不确定的结果。
- 在多线程环境中,当各线程不共享数据的时候,即都是私有(private)成员,那么一定是线程安全的。 但这种情况并不多见,在多数情况下需要共享数据,这时就需要进行适当的同步控制了。
- 线程安全一般都涉及到synchronized, 就是一段代码同时只能有一个线程来操作 不然中间过程可能会 产生不可预制的结果。
- 如果代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运 行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
- 关键字synchronized
当两个并发线程访问同一个对象object中的synchronized(this)同步代码块时,一段时间内只能有一个线程被执行,另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
代码:
同步方法:
- 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象
- 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象
同步代码块:
- 被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
注意:
- 同步代码块中出现异常,锁自动释放
- 同步不能继承,所以还得在子类的方法中添加synchronized关键字
- 当一个线程访问object的一个synchronized同步代码块时,另一个线程仍然可以访问该object对象中的非synchronized(this)同步代码块。
- 在使用同步synchronized(this)代码块时需要注意的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对同一个object中所有其他synchronized(this)同步代码块的访问将被阻塞,这说明synchronized使用的“对象监视器”是一个synchronized方法一样,synchronized(this)代码块也是锁定当前对象的
- 多个线程调用同一个对象中的不同名称的synchronized同步方法或synchronized(this)同步代码块时,调用的效果就是按顺序执行,也就是同步的,阻塞的。
- 锁对象可以是任意对象
- 关键字synchronized还可以应用在static静态方法上,如果这样写,那是对当前的*.java文件对应的Class类进行持锁。
- 异步的原因是持有不同的锁,一个是对象锁,另外一个是Class锁,而Class锁可以对类的所有对象实例起作用。
- 将synchronized(string)同步块与String联合使用时,要注意常量池以带来的一些例外(尽量避免)
1 package day12; 2 3 import java.util.ArrayList; 4 import java.util.List; 5 6 public class _Thread_P2 { 7 public static void main(String[] args) throws InterruptedException { 8 Runnable r = new T(); 9 10 Thread t1 = new Thread(r); 11 Thread t2 = new Thread(r); 12 Thread t3 = new Thread(r); 13 Thread t4 = new Thread(r); 14 Thread t5 = new Thread(r); 15 Thread t6 = new Thread(r); 16 Thread t7 = new Thread(r); 17 Thread t8 = new Thread(r); 18 t1.start(); 19 t2.start(); 20 t3.start(); 21 t4.start(); 22 t5.start(); 23 t6.start(); 24 t7.start(); 25 t8.start(); 26 while(t1.isAlive() || t2.isAlive() || t3.isAlive() || t4.isAlive()) { 27 28 } 29 Thread.sleep(1000); 30 System.out.println(T.l.size()); 31 } 32 } 33 34 class T implements Runnable{ 35 36 static List l = new ArrayList(); 37 38 @Override 39 public void run() { 40 for(int i = 0 ; i < 100 ; i++) { 41 test(i); 42 } 43 } 44 //锁--对象(方法上锁对象是this) 45 public synchronized void test(int i) { 46 l.add(i); 47 } 48 }
1 package day12; 2 3 import java.util.ArrayList; 4 import java.util.List; 5 6 public class _Lock { 7 public static void main(String[] args) throws InterruptedException { 8 Object obj = new Object(); 9 //是同步的,因为锁都是同一个obj对象 10 Thread t1 = new Test(obj); 11 Thread t2 = new Test(obj); 12 Thread t3 = new Test(obj); 13 Thread t4 = new Test(obj); 14 15 //不是同步的,因为每个都传的是一个新的obj 16 // Thread t1 = new Test(new Object()); 17 // Thread t2 = new Test(new Object()); 18 // Thread t3 = new Test(new Object()); 19 // Thread t4 = new Test(new Object()); 20 t1.start(); 21 t2.start(); 22 t3.start(); 23 t4.start(); 24 Thread.sleep(1000); 25 System.out.println(Test.list.size()); 26 } 27 } 28 29 class Test extends Thread{ 30 Object obj; 31 public Test(Object obj) { 32 this.obj = obj; 33 } 34 static List list = new ArrayList(); 35 public void run() { 36 for(int i = 0 ; i < 100 ; i++) { 37 add(i); 38 } 39 } 40 41 public void add(int i) { 42 //(锁对象) 43 //同步必须传入的是一个Object,不能是一个int或者什么的 44 synchronized (obj) { 45 list.add(i); 46 } 47 } 48 }
输出的是 : 400
因为同步了
- wait notify notifyAll
都是和锁有关的
在Object里面
是锁对象的方法
先说两个概念:锁池和等待池
- 锁池:假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。
- 等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁后,进入到了该对象的等待池中
wait(相当于OS中的P()操作)
锁对象的wait()
该线程以后不允许拿到该锁
关到了小黑屋
notify(相当于OS中的V()操作)
唤醒
从线程池中随机选择一个线程唤醒
notifyAll
唤醒全部线程
notify和notifyAll的区别
- 如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
- 当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争
- 优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
wait使用样例
1 try { 2 this.wait(); 3 } catch (InterruptedException e) { 4 // TODO: handle exception 5 }
notify使用样例
1 this.notify();
-
Executor vs ExecutorService vs Executors
- Executor 和 ExecutorService 这两个接口主要的区别是:ExecutorService 接口继承了 Executor 接口,是 Executor 的子接口
- Executor 和 ExecutorService 第二个区别是:Executor 接口定义了
execute()
方法用来接收一个Runnable
接口的对象,而 ExecutorService 接口中的submit()
方法可以接受Runnable
和Callable
接口的对象。 - Executor 和 ExecutorService 接口第三个区别是 Executor 中的
execute()
方法不返回任何结果,而 ExecutorService 中的submit()
方法可以通过一个 Future 对象返回运算结果。 - Executor 和 ExecutorService 接口第四个区别是除了允许客户端提交一个任务,ExecutorService 还提供用来控制线程池的方法。比如:调用
shutDown()
方法终止线程池。 - Executors 类提供工厂方法用来创建不同类型的线程池。比如:
newSingleThreadExecutor()
创建一个只有一个线程的线程池,newFixedThreadPool(int numOfThreads)
来创建固定线程数的线程池,newCachedThreadPool()
可以根据需要创建新的线程,但如果已有线程是空闲的会重用已有线程。
ExecutorService相比于其他两个,拥有更多的方法
里面的submit还可以返回一个泛型,用起来会更加灵活
- ExecutorService
方法
- ExecutorService.shutdown() 方法将允许先前提交的任务在终止之前执行,而 shutdownNow() 方法可防止等待的任务启动并尝试停止当前正在执行的任务。终止后,执行者将没有正在执行的任务,没有正在等待执行的任务,并且无法提交新任务。应该关闭不使用的 ExecutorService 以便回收其资源。如果要关闭ExecutorService中执行的线程,我们可以调用ExecutorService.shutdown()方法。在调用shutdown()方法之后,ExecutorService不会立即关闭,但是它不再接收新的任务,直到当前所有线程执行完成才会关闭,所有在shutdown()执行之前提交的任务都会被执行。
- execute(Runnable) 无法获取执行结果 上一个任务没有完成(第一个线程正在忙),需要另一个线程执行该任务,就另开辟一个线程
- submit(Runnable) 可以判断任务是否完成,如果任务完成,future.get()会返回null,future.get会阻塞。
- submit(Callable)可以获取返回结果
- invokeAny(...)
invokeAny(...)方法接收的是一个Callable的集合,执行这个方法不会返回Future,但是会返回所有Callable任务中其中一个任务的执行结果。这个方法也无法保证返回的是哪个任务的执行结果,反正是其中的某一个
- invokeAll(...)invokeAll(...)与 invokeAny(...)类似也是接收一个Callable集合,但是前者执行之后会返回一个Future的List,其中对应着每个Callable任务执行后的Future对象。
- Future
- 1. Future的应用场景
在并发编程中,我们经常用到非阻塞的模型,在之前的多线程的三种实现中,不管是继承thread类还是实现runnable接口,都无法保证获取到之前的执行结果。通过实现Callback接口,并用Future可以来接收多线程的执行结果。
Future表示一个可能还没有完成的异步任务的结果,针对这个结果可以添加Callback以便在任务执行成功或失败后作出相应的操作。
举个例子:比如去吃早点时,点了包子和凉菜,包子需要等3分钟,凉菜只需1分钟,如果是串行的一个执行,在吃上早点的时候需要等待4分钟,但是因为你在等包子的时候,可以同时准备凉菜,所以在准备凉菜的过程中,可以同时准备包子,这样只需要等待3分钟。那Future这种模式就是后面这种执行模式。
- 2. Future的类图结构
Future接口定义了主要的5个接口方法,有RunnableFuture和SchedualFuture继承这个接口,以及CompleteFuture和ForkJoinTask继承这个接口。
RunnableFuture
这个接口同时继承Future接口和Runnable接口,在成功执行run()方法后,可以通过Future访问执行结果。这个接口都实现类是FutureTask,一个可取消的异步计算,这个类提供了Future的基本实现,后面我们的demo也是用这个类实现,它实现了启动和取消一个计算,查询这个计算是否已完成,恢复计算结果。计算的结果只能在计算已经完成的情况下恢复。如果计算没有完成,get方法会阻塞,一旦计算完成,这个计算将不能被重启和取消,除非调用runAndReset方法。
FutureTask能用来包装一个Callable或Runnable对象,因为它实现了Runnable接口,而且它能被传递到Executor进行执行。为了提供单例类,这个类在创建自定义的工作类时提供了protected构造函数。
SchedualFuture
这个接口表示一个延时的行为可以被取消。通常一个安排好的future是定时任务SchedualedExecutorService的结果
CompleteFuture
一个Future类是显示的完成,而且能被用作一个完成等级,通过它的完成触发支持的依赖函数和行为。当两个或多个线程要执行完成或取消操作时,只有一个能够成功。
ForkJoinTask
基于任务的抽象类,可以通过ForkJoinPool来执行。一个ForkJoinTask是类似于线程实体,但是相对于线程实体是轻量级的。大量的任务和子任务会被ForkJoinPool池中的真实线程挂起来,以某些使用限制为代价。
- 3. Future的主要方法
Future接口主要包括5个方法
- get()方法可以当任务结束后返回一个结果,如果调用时,工作还没有结束,则会阻塞线程,直到任务执行完毕
- get(long timeout,TimeUnit unit)做多等待timeout的时间就会返回结果
- cancel(boolean mayInterruptIfRunning)用来取消任务,如果取消任务成功则返回true,如果取消任务失败则返回false。参数mayInterruptIfRunning表示是否允许取消正在执行却没有执行完毕的任务,如果设置true,则表示可以取消正在执行过程中的任务。如果任务已经完成,则无论mayInterruptIfRunning为true还是false,此方法肯定返回false,即如果取消已经完成的任务会返回false;如果任务正在执行,若mayInterruptIfRunning设置为true,则返回true,若mayInterruptIfRunning设置为false,则返回false;如果任务还没有执行,则无论mayInterruptIfRunning为true还是false,肯定返回true。
- isDone()表示任务是否已经完成,若任务完成,则返回true;
- isCancel()表示任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true。
综合样例:
1 public class _Pool { 2 public static void main(String[] args) { 3 ExecutorService es = Executors.newCachedThreadPool(); 4 //es.execute(new Run());//开辟一个线程执行该任务 5 es.execute(new Runnable() { 6 7 @Override 8 public void run() { 9 // TODO Auto-generated method stub 10 System.out.println(Thread.currentThread().getName()); 11 try { 12 Thread.sleep(70000); 13 } catch (InterruptedException e) { 14 // TODO Auto-generated catch block 15 e.printStackTrace(); 16 } 17 } 18 }); 19 try { 20 Thread.sleep(1000); 21 } catch (InterruptedException e) { 22 // TODO Auto-generated catch block 23 e.printStackTrace(); 24 } 25 es.execute(new Run());//上一个任务没有完成(第一个线程正在忙),需要另一个线程执行该任务,就另开辟一个线程 26 es.shutdown(); 27 } 28 } 29 30 class Run implements Runnable{ 31 @Override 32 public void run() { 33 for(int i = 0 ; i < 10 ; i++) { 34 System.out.println(Thread.currentThread().getName() + " " + i); 35 } 36 } 37 }
运行结果:
1 pool-1-thread-1 2 pool-1-thread-2 0 3 pool-1-thread-2 1 4 pool-1-thread-2 2 5 pool-1-thread-2 3 6 pool-1-thread-2 4 7 pool-1-thread-2 5 8 pool-1-thread-2 6 9 pool-1-thread-2 7 10 pool-1-thread-2 8 11 pool-1-thread-2 9
- 线程池
在执行一个异步任务或并发任务时,往往是通过直接new Thread()方法来创建新的线程,这样做弊端较多,更好的解决方案是合理地利用线程池
线程池的优势
- 降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
- 提高系统响应速度,当有任务到达时,无需等待新线程的创建便能立即执行;
- 方便线程并发数的管控,线程若是无限制的创建,不仅会额外消耗大量系统资源,更是占用过多资源而阻塞系统或oom等状况,从而降低系统的稳定性。线程池能有效管控线程,统一分配、调优,提供资源使用率;
- 更强大的功能,线程池提供了定时、定期以及可控线程数等功能的线程池,使用方便简单。
线程池用法
Java API针对不同需求,利用Executors类提供了4种不同的线程池:newCachedThreadPool, newFixedThreadPool, newScheduledThreadPool, newSingleThreadExecutor
- newCachedThreadPool
创建一个可缓存的无界线程池,该方法无参数。当线程池中的线程空闲时间超过60s则会自动回收该线程,当任务超过线程池的线程数则创建新线程。线程池的大小上限为Integer.MAX_VALUE,可看做是无限大。
- newFixedThreadPool
创建一个固定大小的线程池,该方法可指定线程池的固定大小,对于超出的线程会在LinkedBlockingQueue队列中等待。
- newSingleThreadExecutor
创建只有一个线程的线程池,该方法无参数,所有任务都保存队列LinkedBlockingQueue中,等待唯一的单线程来执行任务,并保证所有任务按照指定顺序执行。
- newScheduledThreadPool
创建一个可定时执行或周期执行任务的线程池,该方法可指定线程池的核心线程个数。
ThreadPoolExecutor
jdk中Executor框架虽然提供了如newFixedThreadPool()、newSingleThreadExecutor()、newCachedThreadPool()等创建线程池的方法,但都有其局限性,不够灵活;另外由于前面几种方法内部也是通过ThreadPoolExecutor方式实现,使用ThreadPoolExecutor有助于大家明确线程池的运行规则,创建符合自己的业务场景需要的线程池,避免资源耗尽的风险。
- 参数说明
- corePoolSize 核心线程大小
线程池中最小的线程数量,即使处理空闲状态,也不会被销毁,除非设置了allowCoreThreadTimeOut。
- CPU密集型:核心线程数 = CPU核数 + 1
IO密集型:核心线程数 = CPU核数 * 2+1
注:IO密集型(某大厂实践经验)
核心线程数 = CPU核数 / (1-阻塞系数)
例如阻塞系数 0.8,CPU核数为4,则核心线程数为20
- maximumPoolSize 线程池最大线程数量
一个任务被提交后,首先会被缓存到工作队列中,等工作队列满了,则会创建一个新线程,处理从工作队列中的取出一个任务。
- keepAliveTime 空闲线程存活时间
当线程数量大于corePoolSize时,一个处于空闲状态的线程,在指定的时间后会被销毁。
- unit 空间线程存活时间单位
keepAliveTime的计量单位
- workQueue 工作队列
任务队列,被添加到线程池中,但尚未被执行的任务;它一般分为直接提交队列、有界任务队列、无界任务队列、优先任务队列几种;
直接提交队列
有界的任务队列
无界的任务队列
优先任务队列
- threadFactory 线程工厂
创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等
- handler 拒绝策略
关键字volatile
被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象。
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后
保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
禁止进行指令重排序。
告诉编译器,这个值是随时会发生变化的,不可以把他放到缓存里面去哦!(保证了数据的可见性)
去内存里面读取他的值,而不是从寄存器里面去取他的值
具体:https://www.cnblogs.com/dolphin0520/p/3920373.html
守护线程
锁
悲观锁与乐观锁
- 锁的一种宏观分类方式是悲观锁和乐观锁。悲观锁与乐观锁并不是特指某个锁(Java中没有哪个Lock实现类就叫PessimisticLock或OptimisticLock),而是在并发情况下的两种不同策略。
- 悲观锁(Pessimistic Lock), 就是很悲观,每次去拿数据的时候都认为别人会修改。所以每次在拿数据的时候都会上锁。这样别人想拿数据就被挡住,直到悲观锁被释放。
- 乐观锁(Optimistic Lock), 就是很乐观,每次去拿数据的时候都认为别人不会修改。所以不会上锁,不会上锁!但是如果想要更新数据,则会在更新前检查在读取至更新这段时间别人有没有修改过这个数据。如果修改过,则重新读取,再次尝试更新,循环上述步骤直到更新成功(当然也允许更新失败的线程放弃操作)。
- 悲观锁阻塞事务,乐观锁回滚重试,它们各有优缺点,不要认为一种一定好于另一种。像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行重试,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。
- 乐观锁的基础——CAS
说到乐观锁,就必须提到一个概念:CAS
什么是CAS呢?Compare-and-Swap,即比较并替换,也有叫做Compare-and-Set的,比较并设置。
比较:读取到了一个值A,在将其更新为B之前,检查原值是否仍为A(未被其他线程改动)。
设置:如果是,将A更新为B,结束。如果不是,则什么都不做。
上面的两步操作是原子性的,可以简单地理解为瞬间完成,在CPU看来就是一步操作。
有了CAS,就可以实现一个乐观锁:
Java中真正的CAS操作调用的native方法因为整个过程中并没有“加锁”和“解锁”操作,因此乐观锁策略也被称为无锁编程。换句话说,乐观锁其实不是“锁”,它仅仅是一个循环重试CAS的算法而已!
- 自旋锁
有一种锁叫自旋锁。所谓自旋,说白了就是一个 while(true) 无限循环(看下面)。
- synchronized锁升级:偏向锁 → 轻量级锁 → 重量级锁
- 前面提到,synchronized关键字就像是汽车的自动档,现在详细讲这个过程。一脚油门踩下去,synchronized会从无锁升级为偏向锁,再升级为轻量级锁,最后升级为重量级锁,就像自动换挡一样。那么自旋锁在哪里呢?这里的轻量级锁就是一种自旋锁。
- 初次执行到synchronized代码块的时候,锁对象变成偏向锁,字面意思是“偏向于第一个获得它的线程”的锁。
- 一旦有第二个线程加入锁竞争,偏向锁就升级为轻量级锁(自旋锁)。这里要明确一下什么是锁竞争:如果多个线程轮流获取一个锁,但是每次获取锁的时候都很顺利,没有发生阻塞,那么就不存在锁竞争。只有当某线程尝试获取锁的时候,发现该锁已经被占用,只能等待其释放,这才发生了锁竞争。
在轻量级锁状态下继续锁竞争,没有抢到锁的线程将自旋,即不停地循环判断锁是否能够被成功获取。获取锁的操作,其实就是通过CAS修改对象头里的锁标志位。先比较当前锁标志位是否为“释放”,如果是则将其设置为“锁定”,比较并设置是原子性发生的。这就算抢到锁了,然后线程将当前锁的持有者信息修改为自己。
长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)。如果多个线程用一个锁,但是没有发生锁竞争,或者发生了很轻微的锁竞争,那么synchronized就用轻量级锁,允许短时间的忙等现象。
显然,此忙等是有限度的(有个计数器记录自旋次数,默认允许循环10次,可以通过虚拟机参数更改)。如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁(依然是CAS修改锁标志位,但不修改持有锁的线程ID)。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待将来被唤醒。在JDK1.6之前,synchronized直接加重量级锁,很明显现在得到了很好的优化。
一个锁只能按照 偏向锁、轻量级锁、重量级锁的顺序逐渐升级(也有叫锁膨胀),不允许降级。
偏向锁的一个特性是,持有锁的线程在执行完同步代码块时不会释放锁。那么当第二个线程执行到这个synchronized代码块时是否一定会发生锁竞争然后升级为轻量级锁呢?
线程A第一次执行完同步代码块后,当线程B尝试获取锁的时候,发现是偏向锁,会判断线程A是否仍然存活。如果线程A仍然存活,将线程A暂停,此时偏向锁升级为轻量级锁,之后线程A继续执行,线程B自旋。但是如果判断结果是线程A不存在了,则线程B持有此偏向锁,锁不升级。
- 可重入锁(递归锁)
可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁。比如一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入锁(因为这个原因可重入锁也叫做递归锁)。
Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重入的。
- 公平锁、非公平锁
- 如果多个线程申请一把公平锁,那么当锁释放的时候,先申请的先得到,非常公平。显然如果是非公平锁,后申请的线程可能先获取到锁,是随机或者按照其他优先级排序的。
- 对ReentrantLock类而言,通过构造函数传参可以指定该锁是否是公平锁,默认是非公平锁。一般情况下,非公平锁的吞吐量比公平锁大,如果没有特殊要求,优先使用非公平锁。
- ReentrantLock构造器可以指定为公平或非公平,对于synchronized而言,它也是一种非公平锁,但是并没有任何办法使其变成公平锁。
- 可中断锁
可中断锁,字面意思是“可以响应中断的锁”。
这里的关键是理解什么是中断。Java并没有提供任何直接中断某线程的方法,只提供了中断机制。何谓“中断机制”?线程A向线程B发出“请你停止运行”的请求(线程B也可以自己给自己发送此请求),但线程B并不会立刻停止运行,而是自行选择合适的时机以自己的方式响应中断,也可以直接忽略此中断。也就是说,Java的中断不能直接终止线程,而是需要被中断的线程自己决定怎么处理。这好比是父母叮嘱在外的子女要注意身体,但子女是否注意身体,怎么注意身体则完全取决于自己。
回到锁的话题上来,如果线程A持有锁,线程B等待获取该锁。由于线程A持有锁的时间过长,线程B不想继续等待了,我们可以让线程B中断自己或者在别的线程里中断它,这种就是可中断锁。
在Java中,synchronized就是不可中断锁,而Lock的实现类都是可中断锁,可以简单看下Lock接口。
- 读写锁、共享锁、互斥锁(了解)
- 读写锁其实是一对锁,一个读锁(共享锁)和一个写锁(互斥锁、排他锁)。
- 看下Java里的ReadWriteLock接口,它只规定了两个方法,一个返回读锁,一个返回写锁。
读写锁其实做的事情是一样的,但是策略稍有不同。很多情况下,线程知道自己读取数据后,是否是为了更新它。那么何不在加锁的时候直接明确这一点呢?如果我读取值是为了更新它(SQL的for update就是这个意思),那么加锁的时候就直接加写锁,我持有写锁的时候别的线程无论读还是写都需要等待;如果我读取数据仅为了前端展示,那么加锁时就明确地加一个读锁,其他线程如果也要加读锁,不需要等待,可以直接获取(读锁计数器+1)。
虽然读写锁感觉与乐观锁有点像,但是读写锁是悲观锁策略。因为读写锁并没有在更新前判断值有没有被修改过,而是在加锁前决定应该用读锁还是写锁。乐观锁特指无锁编程。
JDK提供的唯一一个ReadWriteLock接口实现类是ReentrantReadWriteLock。看名字就知道,它不仅提供了读写锁,而是都是可重入锁。 除了两个接口方法以外,ReentrantReadWriteLock还提供了一些便于外界监控其内部工作状态的方法,这里就不一一展开。
回到悲观锁和乐观锁
我们在Java里使用的各种锁,几乎全都是悲观锁。synchronized从偏向锁、轻量级锁到重量级锁,全是悲观锁。JDK提供的Lock实现类全是悲观锁。其实只要有“锁对象”出现,那么就一定是悲观锁。因为乐观锁不是锁,而是一个在循环里尝试CAS的算法。
那JDK并发包里到底有没有乐观锁呢?
有。java.util.concurrent.atomic包里面的原子类都是利用乐观锁实现的。