本文目录
线程同步简介
需求
原子操作
synchronized简介
JVM原子操作
总结
synchronized使用详解
加锁对象
读取方法
Thread-safe
总结
死锁
死锁现象
死锁形成条件
避免死锁
wait/notify
生产者-消费者
线程同步简介
需求
多个线程同时运行,线程调度由操作系统决定,程序本身无法决定.
当多个线程同时读写同一个共享变量时,就会出现变量的值不准确的现象。
class Counter {
public static int count = 0;
}
class Addthread extends Thread {
@Override
public void run {
for(int i=0;i<10000;i++) {
Counter.count += 1;
}
}
}
class DecThread extends Thread {
@Override
public void run {
for(int i=0;i<10000;i++) {
Counter.count -= 1;
}
}
}
public class Main {
public static void main(String[] args) throws Exception {
Thread t1 = new AddThread();
Thread t2 = new DecThread();
t1.start();
t2.start();
t1.join();
t2.join();
// 多运行几次会发现,count的值不一定是0
System.out.println(Counter.count);
}
}
原子操作
- 对共享变量进行写入时,必须保证是原子操作
- 原子操作是指不能被中断的一个或一系列操作
为了保证一系列操作为原子操作:
- 必须保证一系列操作执行过程中不被其他线程执行。因此可以对操作进行加锁和解锁。
- Java使用synchronized对一个对象进行加锁
synchronized(lock) {
n = n + 1;
}
synchronized简介
特点
- 性能低:同步代码块会消耗资源
- 不用担心异常:即使有异常,同步代码块也能释放锁
基本使用
- 找出修改共享变量的线程代码块
- 选择一个实例作为锁
- 使用synchronized(lock){}
对上例进行修改
class Counter {
public static int count = 0;
}
class Addthread extends Thread {
@Override
public void run {
for(int i=0;i<10000;i++) {
// 加锁
synchronized(Main.LOCK) {
Counter.count += 1;
}
}
}
}
class DecThread extends Thread {
@Override
public void run {
for(int i=0;i<10000;i++) {
// 加锁
synchronized(Main.LOCK) {
Counter.count -= 1;
}
}
}
}
public class Main {
// 定义一个锁
public static final Object LOCK = new Object();
public static void main(String[] args) throws Exception {
Thread t1 = new AddThread();
Thread t2 = new DecThread();
t1.start();
t2.start();
t1.join();
t2.join();
// 多运行几次会发现,count的值始终是0
System.out.println(Counter.count);
}
}
JVM原子操作
类型
- 基本类型赋值(long和double除外)
int n = 1;
- 引用类型赋值
List<String> list = aList;
对原子操作不需要进行同步,如果多个操作需要同步,也可以转换成原子操作,如下:
class Pair {
int first;
int last;
public void set(int first, int last) {
// 使用同步
synchronized(lock) {
this.first = first;
this.last = last;
}
}
}
// 不使用同步
class Pair {
intp[] pair;
public void set(int first, int last) {
int[] ps = new int[]{first, last};
this.pair = ps;
}
}
总结
多线程同时修改同一个变量,会造成逻辑错误:
- 需要通过synchronized同步
- 同步的本质就是给指定对象加锁
- 注意加锁对象必须是同一个实例
- 对JVM定义的单个原子操作不需要同步
synchronized使用详解
加锁对象
加锁对象的选择:把同步逻辑封装到到持有数据的实例中,使用this加锁
synchronized可以用在代码块上,也可以用在方法上,用在方法上表示对整个方法内的代码进行加锁
private int count = 0;
// 对方法加锁
public synchronized void add() {
count += 1;
count -= 1;
}
// 等同于下面
public void add() {
// 对代码块使用当前对象加锁
synchronized(this) {
count += 1;
count -= 1;
}
}
如果对静态方法进行加锁,那么要使用当前对象的Class实例
public class A {
static int count;
static void add(int n) {
// 锁住的是当前类的Class实例
synchronized(A.class) {
count += n;
}
}
}
读取方法
如果只是单纯的一个原子操作进行读取数据,那么可以不加锁。
public int get() {
// 可以进行同步
return this.value;
}
但是如果读取过程较复杂存在线程安全问题,则需要进行加锁。
public synchronized int[] get() {
int[] result = new int[2];
result[0] = this.value[0];
// 如果不使用同步,在读取this.value[0]的时候
// this.value[1]可能会被其他线程修改
result[1] = this.value[1];
return result;
}
Thread-safe
如果一个类被设计为允许多线程正确访问的,那这个类就是线程安全的(thread-safe),比如java.lang.StringBuffer
// StringBuffer的方法都使用了synchronized来标识
// ... ...
@Override
public synchronized int length() {
return count;
}
@Override
public synchronized int capacity() {
return value.length;
}
@Override
public synchronized void ensureCapacity(int minimumCapacity) {
super.ensureCapacity(minimumCapacity);
}
/**
* @since 1.5
*/
@Override
public synchronized void trimToSize() {
super.trimToSize();
}
/**
* @throws IndexOutOfBoundsException {@inheritDoc}
* @see #length()
*/
@Override
public synchronized void setLength(int newLength) {
toStringCache = null;
super.setLength(newLength);
}
// ... ...
线程安全的类:
- 不变的类:String,Integer,LocalDate
- 没有成员变量的类(多是工具类):Math
- 正确使用synchronized的类:StringBuffer
非线程安全的类:
- 不能在多线程中共享实例并修改:ArrayList
- 可以在多线程中以只读方式共享
总结
- 用synchronized修饰方法可以把整个方法变为同步代码块
- synchronized方法加锁对象是this
- 通过合理的设计和数据封装可以让一个类变为“线程安全”
- 一个类没有特殊说明,默认不是thread-safe
- 多线程能否访问某个非线程安全的实例,需要具体情况具体分析
死锁
死锁现象
要执行synchronized代码块,必须要先获得指定对象的锁才能运行。
Java的线程锁是可重入的锁,即获取到一个对象的锁的synchronized代码块中再次获取这个对象的锁
public void add(int n) {
synchronized(lock) {
this.value += n;
// 调用另一个加了相同锁的方法
addAnother(n);
}
}
public void addAnother(int m) {
// 获取同一个锁
synchronized(lock) {
this.another += m;
}
}
// 上面代码等同于
public void add(int m) {
synchronized(lock) {
this.value += m;
synchronized(lock) {
this.another += m;
}
}
}
也可以是两个不同的锁
public void add(int m) {
// 获取lockA的锁
synchronized(lockA) {
this.value += m;
// 获取lockB的锁
synchronized(lockB) {
this.another += m;
}// 释放lockB的锁
}// 释放lockA的锁
}
当不同线程获取多个不同对象的锁可能导致死锁
public void add(int m) {
synchronized(lockA) {
this.value += m;
// 等待获取lockB的锁
synchronized(lockB) {
this.another += m;
}
}
}
public void add(int m) {
synchronized(lockB) {
this.value += m;
// 等待获取lockA的锁
synchronized(lockA) {
this.another += m;
}
}
}
当两个线程分别执行以上代码时,线程A获取到lockA的锁,线程B获取到lockB的锁,然后线程A开始等待获取lockB的锁,而线程B开始等待获取lockA的锁,两个线程陷入互相等待的僵局,旧形成了死锁
死锁形成条件
- 两个线程各自持有不同的锁
- 两个线程各自试图获取对方已持有的锁
- 双方无限等待下去,导致死锁
死锁处理
- 没有任何机制能解除死锁
- 只能强制结束JVM进程
避免死锁
- 多线程获取锁的顺序要一致
wait/notify
synchronized解决了多线程竞争的问题,但没有解决多线程协调的问题,比较典型的生产者消费者问题,消费者必须在有产品时才能消费,如果没有就必须等待
生产者-消费者
// 一个简单的生产者-消费者案例
// 仓库
class Repository {
private LinkedList<Object> list = new LinkedList<>();
public synchronized void produce() {
list.add("1");
System.out.println("生产一个,当前有" + list.size() + "个");
// 唤醒所有等待的消费者
this.notifyAll();
}
public synchronized void consume() {
while (list.size() == 0) {
try {
// 消费者开始等待
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
return;
}
}
list.remove();
System.out.println("消费一个,当前有" + list.size() + "个");
}
}
// 生产者
class Producer extends Thread {
private Repository repository;
public Producer(Repository repository) {
this.repository = repository;
}
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
repository.produce();
}
}
// 消费者
class Consumer extends Thread {
private Repository repository;
public Consumer(Repository repository) {
this.repository = repository;
}
@Override
public void run() {
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
repository.consume();
}
}
// 主类
public class Main {
public static void main(String[] args) {
// 创建公共仓库
Repository repository = new Repository();
// 创建生产者
Producer producer1 = new Producer(repository);
Producer producer2 = new Producer(repository);
Producer producer3 = new Producer(repository);
// 创建消费者
Consumer consumer1 = new Consumer(repository);
Consumer consumer2 = new Consumer(repository);
Consumer consumer3 = new Consumer(repository);
// 开始工作
producer1.start();
producer2.start();
producer3.start();
consumer1.start();
consumer2.start();
consumer3.start();
}
}
// 运行结果(顺序不固定)
生产一个,当前有1个
消费一个,当前有0个
生产一个,当前有1个
生产一个,当前有2个
消费一个,当前有1个
消费一个,当前有0个
可以看出,如果消费者不进行等待,那么会出现消费的产品为空,并且线程会结束,使用wait()进行等待,当仓库中有产品时,生产者再调用notify/notifyAll来唤醒等待的线程,这样消费者就可以继续消费了。