Java多线程之线程安全
保证线程安全的方法
- Synchronized,同步方法,同步块
- Volatile,修饰变量
- Lock,可重入锁,读写锁
- 不变类,final class,很少使用
- 线程安全类
- 不使用共享变量
- ThreadLocal
线程安全类
- Vector、Hashtable,如果不涉及线程安全,用这些类就会浪费资源
- 用静态同步方法封装非同步集合
// Collections中有很多静态的同步方法,来同步那些非同步的集合 List list = Collections.synchronizedList(new ArrayList());
ThreadLocal
给每个线程一个变量拷贝,由线程自己来维护,导致每个线程间的这个变量相互独立,也就没有了共享变量,所以线程安全。
在高并发场景:如果不考虑延迟、共享数据,这会是个不错的选择。
不可变对象
不可变对象可以在没有同步的情况下共享,降低访问时的同步 开销。
创建不可变类
- 全部变量都是私有的
- 通过构造函数初始化私有变量
- 没有setter方法,只有getter方法
- getter方法不要直接返回对象本身,要返回克隆对象
线程安全单例
对于单例模式,我们需要注意的是,有一些单例的写法是线程安全的,而有些是非线程安全的。
饿汉单例(线程安全)
// 在类装载时就实例化 public class Singleton { private static Singleton sin=new Singleton(); ///直接初始化一个实例对象 private Singleton(){ ///private类型的构造函数,保证其他类对象不能直接new一个该对象的实例 } public static Singleton getSin(){ ///该类唯一的一个public方法 return sin; } }
懒汉单例(非线程安全)
// 在使用时再实例化 public class Singleton { private static Singleton instance; private Singleton (){} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
加锁的懒汉单例(线程安全)
// 这种加锁的方式虽然能解决线程安全,但是影响性能 public class Singleton { private static Singleton instance; private Singleton (){} public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
双重校验锁的懒汉单例(线程安全)
// 保证了线程安全和性能 public class Singleton { private volatile static Singleton singleton; private Singleton (){} public static Singleton getSingleton() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
常用的同步类
- CountDownLatch:闭锁,运动员计时
- Semaphone:信号量,并发资源访问控制
- CyclicBarrier:栅栏,游戏中等待所有人到了,再进入下一关
- Phaser:一种可重用的同步屏障,功能上类似于CyclicBarrier和CountDownLatch,但使用上更为灵活。
- Exchanger:允许两个线程在某个汇合点交换对象,在某些管道设计时比较有用。
CountDownLatch
一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。
主要方法
// 构造方法参数指定了计数的次数 public CountDownLatch(int count); // 当前线程调用此方法,则计数减一 public void countDown(); // 调用此方法会一直阻塞当前线程,直到计时器的值为0 public void await() throws InterruptedException
代码
public class CountDownLatchDemo { final static SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static void main(String[] args) throws InterruptedException { CountDownLatch latch=new CountDownLatch(2);//两个工人的协作 Worker worker1=new Worker("zhang san", 5000, latch); Worker worker2=new Worker("li si", 8000, latch); worker1.start();// worker2.start();// latch.await();//等待所有工人完成工作 System.out.println("all work done at "+sdf.format(new Date())); } static class Worker extends Thread{ String workerName; int workTime; CountDownLatch latch; public Worker(String workerName ,int workTime ,CountDownLatch latch){ this.workerName=workerName; this.workTime=workTime; this.latch=latch; } public void run(){ System.out.println("Worker "+workerName+" do work begin at "+sdf.format(new Date())); doWork();//工作了 System.out.println("Worker "+workerName+" do work complete at "+sdf.format(new Date())); latch.countDown();//工人完成工作,计数器减一 } private void doWork(){ try { Thread.sleep(workTime); } catch (InterruptedException e) { e.printStackTrace(); } } } }
结果
Worker zhang san do work begin at 2011-04-14 11:05:11
Worker li si do work begin at 2011-04-14 11:05:11
Worker zhang san do work complete at 2011-04-14 11:05:16
Worker li si do work complete at 2011-04-14 11:05:19
all work done at 2011-04-14 11:05:19
Semaphore
厕所有5个坑,假如有10个人要上厕所,那么同时只能有多少个人去上厕所呢?同时只能有5个人能够占用,当5个人中 的任何一个人让开后,其中等待的另外5个人中又有一个人可以占用了。另外等待的5个人中可以是随机获得优先机会,也可以是按照先来后到的顺序获得机会,这取决于构造Semaphore对象时传入的参数选项。单个信号量的Semaphore对象可以实现互斥锁的功能,并且可以是由一个线程获得了“锁”,再由另一个线程释放“锁”,这可应用于死锁恢复的一些场合。
主要方法
// 构造函数,定义只有5个信号量 Semaphore(5) // 获取一个许可,如果没有就等待 acquire() // 释放一个许可 release()
代码
package com.test; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; public class TestSemaphore { public static void main(String[] args) { // 线程池 ExecutorService exec = Executors.newCachedThreadPool(); // 只能5个线程同时访问 final Semaphore semp = new Semaphore(5); // 模拟20个客户端访问 for (int index = 0; index < 20; index++) { final int NO = index; Runnable run = new Runnable() { public void run() { try { // 获取许可 semp.acquire(); System.out.println("Accessing: " + NO); Thread.sleep((long) (Math.random() * 10000)); // 访问完后,释放 semp.release(); System.out.println("-----------------"+semp.availablePermits()); } catch (InterruptedException e) { e.printStackTrace(); } } }; exec.execute(run); } // 退出线程池 exec.shutdown(); } }
结果
Accessing: 0
Accessing: 1
Accessing: 3
Accessing: 4
Accessing: 2
-----------------0
Accessing: 6
-----------------1
Accessing: 7
-----------------1
Accessing: 8
-----------------1
Accessing: 10
-----------------1
Accessing: 9
-----------------1
Accessing: 5
-----------------1
Accessing: 12
-----------------1
Accessing: 11
-----------------1
Accessing: 13
-----------------1
Accessing: 14
-----------------1
Accessing: 15
-----------------1
Accessing: 16
-----------------1
Accessing: 17
-----------------1
Accessing: 18
-----------------1
Accessing: 19
CyclicBarrier
循环障栅栏,一组线程写操作,并且只有所有线程完成写操作,才继续做后面的事
常用方法
// 构造函数,定义三个线程 CyclicBarrier(3); // 当所有参与者都调用await()之前,前面执行完的线程一直等待 await()
代码
public class CyclicBarrierTest { public static void main(String[] args) throws IOException, InterruptedException { //如果将参数改为4,但是下面只加入了3个选手,这永远等待下去 //Waits until all parties have invoked await on this barrier. CyclicBarrier barrier = new CyclicBarrier(3); ExecutorService executor = Executors.newFixedThreadPool(3); executor.submit(new Thread(new Runner(barrier, "1号选手"))); executor.submit(new Thread(new Runner(barrier, "2号选手"))); executor.submit(new Thread(new Runner(barrier, "3号选手"))); executor.shutdown(); } } class Runner implements Runnable { // 一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point) private CyclicBarrier barrier; private String name; public Runner(CyclicBarrier barrier, String name) { super(); this.barrier = barrier; this.name = name; } @Override public void run() { try { Thread.sleep(1000 * (new Random()).nextInt(8)); System.out.println(name + " 准备好了..."); // barrier的await方法,在所有参与者都已经在此 barrier 上调用 await 方法之前,将一直等待。 barrier.await(); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } System.out.println(name + " 起跑!"); } }
结果
3号选手 准备好了...
2号选手 准备好了...
1号选手 准备好了...
1号选手 起跑!
2号选手 起跑!
3号选手 起跑!
为什么只有ConcurrentHashMap,没有Concurrent的List?
从上面的描述,可以体现出Concurrent 比 CopyOnWrite要好,因为保证线程安全和并发瓶颈,那为什么List没有Concurrent实习那呢?
原因
Concurrent的List很难实现,例如:contains() 这样一个操作来说,当你进行搜索的时候如何避免锁住整个list?
同时,这也与ConcurrentHashMap的实现方式有关,一个ConcurrentHashMap由多个segment组成,每一个segment都包含了一个HashEntry数组的hashtable, 每一个segment包含了对自己的hashtable的操作,比如get,put,replace等操作,这些操作发生的时候,对自己的hashtable进行锁定。由于每一个segment写操作只锁定自己的hashtable,所以可能存在多个线程同时写的情况,性能无疑好于只有一个hashtable锁定的情况。
同理,由于HashTable的value不能为null,所以就没有ConcurrentHashSet。