1、高并发线程基础,自旋锁CAS操作与volatile
看一段代码:
import java.util.concurrent.TimeUnit; public class T01_WhatIsThread { private static class T1 extends Thread { @Override public void run() { for(int i=0; i<10; i++) { try { TimeUnit.MICROSECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("T1"); } } } public static void main(String[] args) { new T1().run(); // new T1().start(); for(int i=0; i<10; i++) { try { TimeUnit.MICROSECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("main"); } } }
可以看到线程调用方法:1、run();方法,2、start();方法
这两个方法有什么区别呢?
如果调用run() 方法:可以看到T1 执行完后,才执行main线程
如果调用start()方法:可以看到可以交叉执行
线程有多个方法,如:seelp(); 休眠, yield(); 让行 join(); 抢行
synchronized
锁对象
public class T { private int count = 10; private Object o = new Object(); public void m() { synchronized(o) { //任何线程要执行下面的代码,必须先拿到o的锁 count--; System.out.println(Thread.currentThread().getName() + " count = " + count); } } }
锁定当前对象
public class T { private int count = 10; public void m() { synchronized(this) { //任何线程要执行下面的代码,必须先拿到this的锁 count--; System.out.println(Thread.currentThread().getName() + " count = " + count); } } }
如果锁定当前对象,其实可以直接这样写
public class T { private int count = 10; public synchronized void m() { //等同于在方法的代码执行时要synchronized(this) count--; System.out.println(Thread.currentThread().getName() + " count = " + count); } }
锁静态的写法
public class T { private static int count = 10; public synchronized static void m() { //这里等同于synchronized(FineCoarseLock.class) count--; System.out.println(Thread.currentThread().getName() + " count = " + count); } public static void mm() { synchronized(T.class) { //考虑一下这里写synchronized(this)是否可以? count --; } } }
加锁的时候,可以不加volatile 也可以,这种的不管用volatile 或者synchronized 都可以保证有序
public class T implements Runnable { private /*volatile*/ int count = 100; @Override public /*synchronized*/ void run() { count--; System.out.println(Thread.currentThread().getName() + " count = " + count); } public static void main(String[] args) { T t = new T(); for(int i=0; i<100; i++) { new Thread(t, "THREAD" + i).start(); } } }
同步和非同步方法是否可以同时调用?
public class T { public synchronized void m1() { System.out.println(Thread.currentThread().getName() + " m1 start..."); try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " m1 end"); } public void m2() { try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " m2 "); } public static void main(String[] args) { T t = new T(); /*new Thread(()->t.m1(), "t1").start(); new Thread(()->t.m2(), "t2").start();*/ new Thread(t::m1, "t1").start(); new Thread(t::m2, "t2").start(); /* //1.8之前的写法 new Thread(new Runnable() { @Override public void run() { t.m1(); } }); */ } }
面试题:模拟银行账户
对业务写方法加锁
对业务读方法不加锁
这样行不行?容易产生脏读问题(dirtyRead)
import java.util.concurrent.TimeUnit; public class Account { String name; double balance; public synchronized void set(String name, double balance) { this.name = name; try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } this.balance = balance; } public /*synchronized*/ double getBalance(String name) { // 这个synchronized必须加上才可以保证数据读取的正确 return this.balance; }
public static void main(String[] args) { Account a = new Account(); new Thread(()->a.set("zhangsan", 100.0)).start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(a.getBalance("zhangsan")); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(a.getBalance("zhangsan")); } }
一个同步方法可以调用另外一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁.也就是说synchronized获得的锁是可重入的,它必须可重入
import java.util.concurrent.TimeUnit; public class T { synchronized void m1() { System.out.println("m1 start"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } m2(); // m1方法里可以调用m2方法 System.out.println("m1 end"); } synchronized void m2() { try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("m2"); } public static void main(String[] args) { new T().m1(); } }
synchronized获得的锁是可重入的,这里是继承中有可能发生的情形,子类调用父类的同步方法
import java.util.concurrent.TimeUnit; public class T { synchronized void m() { System.out.println("m start"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("m end"); } public static void main(String[] args) { new TT().m(); } } class TT extends T { @Override synchronized void m() { // 锁的其实还是同一个对象 System.out.println("child m start"); super.m(); System.out.println("child m end"); } }
程序在执行过程中,如果出现异常,默认情况锁会被释放
所以,在并发处理的过程中,有异常要多加小心,不然可能会发生不一致的情况。
比如,在一个web app处理过程中,多个servlet线程共同访问同一个资源,这时如果异常处理不合适,
在第一个线程中抛出异常,其他线程就会进入同步代码区,有可能会访问到异常产生时的数据。
因此要非常小心的处理同步业务逻辑中的异常
import java.util.concurrent.TimeUnit; public class T { int count = 0; synchronized void m() { System.out.println(Thread.currentThread().getName() + " start"); while(true) { count ++; System.out.println(Thread.currentThread().getName() + " count = " + count); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } if(count == 5) { int i = 1/0; //此处抛出异常,锁将被释放,要想不被释放,可以在这里进行catch,然后让循环继续 System.out.println(i); } } } public static void main(String[] args) { T t = new T(); Runnable r = new Runnable() { @Override public void run() { t.m(); } }; new Thread(r, "t1").start(); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(r, "t2").start(); } }
synchronized的底层实现
JDK早期的: 重量级 - OS
后来的改进:锁升级的概念:推荐看《我就是厕所所长》 (一 二)
升级步骤概念:
sync (Object)
markword 在锁的对象头上面记录这个线程ID (偏向锁)
如果线程争用:升级为 自旋锁
10次以后,升级为重量级锁 - OS
不能降级,只能升级
执行时间短(加锁代码),线程数少,--用自旋
执行时间长,线程数多,--用系统锁
不要锁字符常量(如:“abc”)、引用类型,如:Integer、Long
volatile 关键字,使一个变量在多个线程间可见
A B线程都用到一个变量,java默认是A线程中保留一份copy,这样如果B线程修改了该变量,则A线程未必知道
使用volatile关键字,会让所有线程都会读到变量的修改值
在下面的代码中,running是存在于堆内存的t对象中
当线程t1开始运行的时候,会把running值从内存中读到t1线程的工作区,在运行过程中直接使用这个copy,并不会每次都去
读取堆内存,这样,当主线程修改running的值之后,t1线程感知不到,所以不会停止运行
使用volatile,将会强制所有线程都去堆内存中读取running的值
可以阅读这篇文章进行更深入的理解:http://www.cnblogs.com/nexiyi/p/java_memory_model_and_thread.html
volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能替代synchronized
public class T01_HelloVolatile { /*volatile*/ boolean running = true; //对比一下有无volatile的情况下,整个程序运行结果的区别 void m() { System.out.println("m start"); while(running) { } System.out.println("m end!"); } public static void main(String[] args) { T01_HelloVolatile t = new T01_HelloVolatile(); new Thread(t::m, "t1").start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } t.running = false; } }
看这段代码,虽然加了volatile,但是执行结果绝对不是10000,因为count++ 不是原子性操作
import java.util.ArrayList; import java.util.List; public class T04_VolatileNotSync { volatile int count = 0; void m() { for(int i=0; i<10000; i++) { count++; } } public static void main(String[] args) { T04_VolatileNotSync t = new T04_VolatileNotSync(); List<Thread> threads = new ArrayList<Thread>(); for(int i=0; i<10; i++) { threads.add(new Thread(t::m, "thread-"+i)); } threads.forEach((o)->o.start()); threads.forEach((o)->{ try { o.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); System.out.println(t.count); } }
对比上一个程序,可以用synchronized解决,synchronized可以保证可见性和原子性,volatile只能保证可见性
import java.util.ArrayList; import java.util.List; public class T05_VolatileVsSync { /*volatile*/ int count = 0; synchronized void m() { for (int i = 0; i < 10000; i++) { count++; } } public static void main(String[] args) { T05_VolatileVsSync t = new T05_VolatileVsSync(); List<Thread> threads = new ArrayList<Thread>(); for (int i = 0; i < 10; i++) { threads.add(new Thread(t::m, "thread-" + i)); } threads.forEach((o) -> o.start()); threads.forEach((o) -> { try { o.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); System.out.println(t.count); } }
synchronized优化:同步代码块中的语句越少越好,比较m1和m2
import java.util.concurrent.TimeUnit; public class FineCoarseLock { int count = 0; synchronized void m1() { try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } //业务逻辑中只有下面这句需要sync,这时不应该给整个方法上锁 count ++; try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } } void m2() { try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } //业务逻辑中只有下面这句需要sync,这时不应该给整个方法上锁 //采用细粒度的锁,可以使线程争用时间变短,从而提高效率 synchronized(this) { count ++; } try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } } }
锁定某对象o,如果o的属性发生改变,不影响锁的使用,但是如果o变成另外一个对象,则锁定的对象发生改变,应该避免将锁定对象的引用变成另外的对象
import java.util.concurrent.TimeUnit; public class SyncSameObject { /*final*/ Object o = new Object(); void m() { synchronized(o) { while(true) { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()); } } } public static void main(String[] args) { SyncSameObject t = new SyncSameObject(); //启动第一个线程 new Thread(t::m, "t1").start(); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } //创建第二个线程 Thread t2 = new Thread(t::m, "t2"); t.o = new Object(); //锁对象发生改变,所以t2线程得以执行,如果注释掉这句话,线程2将永远得不到执行机会 t2.start(); } }
解决同样的问题的更高效的方法,使用AtomXXX类
AtomXXX类本身方法都是原子性的,但不能保证多个方法连续调用是原子性的
import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; public class T01_AtomicInteger { /*volatile*/ //int count1 = 0; AtomicInteger count = new AtomicInteger(0); /*synchronized*/ void m() { for (int i = 0; i < 10000; i++) //if count1.get() < 1000 count.incrementAndGet(); //count1++,自增 } public static void main(String[] args) { T01_AtomicInteger t = new T01_AtomicInteger(); List<Thread> threads = new ArrayList<Thread>(); for (int i = 0; i < 10; i++) { threads.add(new Thread(t::m, "thread-" + i)); } threads.forEach((o) -> o.start()); threads.forEach((o) -> { try { o.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); System.out.println(t.count); } }
CAS (compare and set)--比较并且设定,无锁优化,自旋、乐观锁
cas 是cpu原语的支持,过程中不能打断
ABA问题,解决方案:每次执行时候加上版本号。可以用AtomicStampedReference解决,加上时间戳。
Unsafe类:等同于C和C++,可以直接操作java虚拟机里面的内存,它只能用反射才能使用:参考:https://blog.csdn.net/zyzzxycj/article/details/89877863
所有的AtomicXXX类,内部使用的都是compareAndSetXXX方法,这些方法都用的Unsafe类里的方法,Unsafe 类是final 的,所以要用才能反射使用。
示例代码:AtomicInteger的使用
import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; public class T01_AtomicInteger { /*volatile*/ //int count1 = 0; AtomicInteger count = new AtomicInteger(0); /*synchronized*/ void m() { // 原来要加synchronized,现在不需要加了 for (int i = 0; i < 10000; i++) //if count1.get() < 1000 count.incrementAndGet(); //count1++ } public static void main(String[] args) { T01_AtomicInteger t = new T01_AtomicInteger(); List<Thread> threads = new ArrayList<Thread>(); for (int i = 0; i < 10; i++) { threads.add(new Thread(t::m, "thread-" + i)); } threads.forEach((o) -> o.start()); threads.forEach((o) -> { try { o.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); System.out.println(t.count); } }
点进去看AtomicInteger的源码,里面有个compareAndSet方法
/** * Atomically sets the value to the given updated value * if the current value {@code ==} the expected value. * * @param expect the expected value * @param update the new value * @return {@code true} if successful. False return indicates that * the actual value was not equal to the expected value. */ public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }
compareAndSet方法里调用的是unsafe类的方法,所以最终调用的是unsafe类,版本问题,有的里面可以是compareAndSwap方法,有的可能是compareAndSet方法,
compareAndSet方法里有3个参数:要改的值、期望的值、设置的新值
如:cas方法(V, Expected, NewValue),
如果第一个参数是要改的值,比如是0,
第二个参数是期望的值,比如现在是3,也就是要这个值的之前,它是3,
第三个参数是1,也就是要把0改成1,
但是如果这个时候有其他线程已经把某个变量改成4了,但是它的期望值是3,所以就会失败,
如果期望值是3,并且中间没有其他线程改这个值,满足期望值的条件,就会修改成功。
如果在修改复制操作的一瞬间,有其他线程要修改这个变量的值,是否会出问题?
不会的,CAS是CPU原语的支持,在执行过程中不允许被其他线程给打断,所以不会出现其他线程的操作。
ABA问题:
在比对期望值的时候,虽然期望值一样的,但是这个过程中有很多线程,
比如期望值是A,其他线程已经改成了B,但是又改成了A,等对比期望值的时候,还以为一直是A,没有发生过改变。
如果是基本数据类型,无所谓,但如果是引用类型,可能指向的对象已经发生改变。
解决方案:
1. 每次执行时候加上版本号,任何线程在执行完以后,要在对应的版本号加1,每次不光比对期望值,还要比对版本号。
2. 可以用AtomicStampedReference解决,加上时间戳。