java并发有两本很好的书《java并发编程的艺术》、《java并发实战》。本文是重读前者把旧笔记再整理一遍而成。
——引言
(1)volatile与synchronized
以读一段代码的方式看下:
/** 单例模式下,如何保证并发安全:两个方案:synchronized;双重锁。
* Created by baimq on 2019/3/16.
*/
public class singleton {
//volatile保证在缓存行和内存层面的读一致。
private static volatile singleton instance;
public static singleton getInstance(){
if(instance==null){
synchronized (singleton.class){//synchronized保证应用层面的代码访问原子性。
if(instance==null){
instance=new singleton();//因为创建对象,并不原子。所以必须保证可见性。
}
}
}
return instance;
}
}
volitile:保证在多线程环境下读的可见性和读的一致性。如果多线程共享的变量不实现volitile,则会导致在A-CPU中读取到M值,随后在B-CPU中读取到N值。但因其不能保证写的原子性,所以不能用来实现内存计数器。
通过lock命令执行MESI协议,保证在CPU和内存层面的读写一致性。其在应用层面不能保证读写一致性。所以使用场景受限:
A、对变量的写操作不依赖于当前值。
B、该变量没有包含在具有其他变量的不变式中。
[MESI:缓存一致性协议。CPU缓冲行与内存之间的数据一致性协议。实现LOCK命令,总线锁或者缓存锁。]
synchronized:用来锁代码块或者方法,保证同一时间只有一个线程能访问到代码块。
Synchronized锁的升级:偏向锁,轻量级锁,重量级锁。
偏向锁:在对象头中写入线程号。
轻量级锁:通过CAS操作实现,如果不能获得锁,则自旋。自旋次数过多,则升级为重量级锁。
重量级锁:通过线程阻塞和唤醒等待线程的方式实现。
解读代码:
利用synchronized来保证只有一个线程可以执行创建对象的动作。
但是漏洞是创建对象并且赋值的过程不是原子的,所以可能发生还没创建好对象,就执行到了return instance阶段。
所以要再利用volitile来保证多个线程的读一致,保证不会在多个线程中读到不一致的结果。
(2)线程的生命周期、对象的锁
thread.join:等待线程执行完毕。
thead.start:开始执行。
thead. Interrupt::安全中断(并没有真中断,而是使线程的isInterrupted返回false)。
thead. isInterrupted()[常用语法:Thread.currentThread().isInterrupted(),用作安全终止线程]。
thread.run,仅执行runnable的run方法一次,与开启线程无关。
对象.wait();//让持有对象线程等待
对象.notify();//随机唤醒持有对象的线程之一。
对象notifyAll();//唤醒持有对象的多个线程。
(3)Lock
Lock接口,四种锁的方式:
lock()阻塞性的锁;
尝试非阻塞地获取锁;
能被中断的获取锁;
超时获取锁。
重入锁:可以多次锁,释放时也需要多次释放。
读写锁:在写的过程中阻塞所有读和写。适合读多写少的场景。
(4)ThreadLocal
为对象的共享变量创建本线程的副本。SimpleDateFormat是非线程安全的类,包装下就安全了,原理见我其他博客:
public class SafeDateUtil { private static ThreadLocal<SimpleDateFormat> threadLocalSDF = new ThreadLocal<SimpleDateFormat>() { @Override protected SimpleDateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); } }; public static String formatDate(Date date)throws ParseException{ return threadLocalSDF.get().format(date); } }
(5)CountDownLatch、CyclicBarrier(同步屏障)、semaphore(信号量、限流器)
CountDownLatch是等所有线程执行完;
CyclicBarrier是等待在线程内屏障点上。
CyclicBarrier更强大,可以完全hold住CountDownLatch的功能。
一个简单的例子把:
/** * Created by baimq on 2019/3/16. */ public class CountDownDemo { public static void main(String[] args) { CountDownLatch countDownLatch=new CountDownLatch(3); new Thread(new WorkerRunnable(countDownLatch,30)).start(); new Thread(new WorkerRunnable(countDownLatch,1000)).start(); new Thread(new WorkerRunnable(countDownLatch,1)).start(); try { Thread.sleep(20); System.out.println("在计数中,干完活的有:"+countDownLatch.getCount()+"个"); countDownLatch.await(); System.out.println("全部干完活了"); } catch (InterruptedException e) { e.printStackTrace(); } } public static class WorkerRunnable implements Runnable{ private CountDownLatch countDownLatch; private int sleep; public WorkerRunnable(CountDownLatch countDownLatch,int sleep){ this.countDownLatch=countDownLatch; this.sleep=sleep; } @Override public void run() { System.out.println(Thread.currentThread().getName()+"在干活"); try { Thread.sleep(sleep); System.out.println(Thread.currentThread().getName()+"干完活了"); } catch (InterruptedException e) { e.printStackTrace(); } countDownLatch.countDown(); } } }
semaphore是限流器保证只有N个线程在同时执行。
(6)Fork/join、并发容器、executor框架
这些很早就在用了,所以比较熟。
fork/join是为了解决任务切割执行。
并发容器如concurrentHashMap等是分区思路,切割为较小的segment,并发写时对segment锁住。
executor是线程池执行框架,支持设置线程容量、设置拒绝策略,设置队列延后执行等。