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。