首先,需要理解一些理论上的东西。
多个线程并发对同一个资源进行操作,很可能发生内存一致性错误。
究其原因,线程很多情况下对资源的操作不是原子的,这些代码会被分为若干条指令去执行,而在一个CPU时间片内又不能将这些指令全部执行完毕。
当多个线程同时操作同一个共享资源时,线程B拿着线程A的半成品进行操作,内存一致性错误就发生了。
如何解决?
1.同步,即:加锁。通过加锁的方式,可以确保唯一获取锁的线程可以不受干扰的执行完自己的程序片段。只有当前线程释放锁之后,其它线程才能对此资源进行操作。否则,只能等待当前线程执行完毕后释放锁(等待锁的线程全部被阻塞了);
2.资源不可变,即:只读。只读意味着数据不会变化,因此对只读数据的操作不可能发生内存不一致问题。
同步锁
同步机制的建立是基于其内部一个叫内部锁或者监视锁的实体。
内部锁在同步机制中起到两方面的作用:
对一个对象的排他性访问;
建立一种happens-before关系,而这种关系正是可见性问题的关键所在。
每个对象都有一个与之关联的内部锁。
当一个线程需要排他性的访问一个对象的域时,首先请求该对象的内部锁,当访问结束时释放内部锁。
在线程获得内部锁到释放内部锁的这段时间里,只有当前线程拥有这个内部锁。
当一个线程拥有一个内部锁时,其他线程将无法获得该内部锁。其他线程如果去尝试获得该内部锁,则会被阻塞。
当线程释放其拥有的内部锁时,该操作和对该锁的后续请求间将建立happens-before关系。
同步(synchronized)的两种方式:
1. 同步方法
非静态方法-this
当线程调用一个同步方法时,它会自动请求该方法所在对象的内部锁。
当方法返回结束时则自动释放该内部锁,即使退出是由于发生了未捕获的异常,内部锁也会被释放。
静态方法-Class
当调用一个静态的同步方法时,由于静态方法是和类(而不是对象)相关的,所以线程会请求类对象(Class Object)的内部锁。
因此用来控制类的静态域访问的锁不同于控制对象访问的锁。
2. 同步代码块
同步块必须指定所请求的是哪个对象的内部锁
可以实现更细粒度的控制
可重入同步
一个线程不能获得其他线程所拥有的锁,但是它可以获得自己已经拥有的锁。
允许一个线程多次获得同一个锁实现了可重入同步。
避免了线程自己阻塞自己。
线程同步可以解决内存不一致错误,但也引入了其它的问题
死锁、饥饿、活锁
A.线程死锁
两个或多个线程永久阻塞,互相等待对方释放资源。
package org.thread;
public class DeadLock {
public static void main(String[] args) {
final Friend zs = new Friend("zs");
final Friend ls = new Friend("ls");
new Thread(new Runnable() {
@Override
public void run() {
while(true)
zs.bow(ls);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while(true)
ls.bow(zs);
}
}).start();
}
static class Friend {
private String name;
public Friend(String name) {
this.name = name;
}
public synchronized void bow(Friend friend) {
System.out.println(Thread.currentThread().getName()+":::"+this.name+" bow to " + friend.name);
friend.bowBack(this);//同步中调用其它对象的同步方法可能会发生死锁:相互等待对方释放锁
System.out.println(Thread.currentThread().getName()+":::"+"bow done!");
}
public synchronized void bowBack(Friend friend) {
System.out.println(Thread.currentThread().getName()+":::"+this.name+" bow to " + friend.name);
}
}
}
引起死锁的原因:需要相互获取对方锁的线程同时占有了自己的锁,导致对方无法获取到锁。
解决办法:通过设置同一个锁实现排他性访问,不给线程同时占有锁的机会。
package org.thread;
public class AvoidDeadLock {
public static void main(String[] args) {
final Friend zs = new Friend("zs");
final Friend ls = new Friend("ls");
new Thread(new Runnable() {
@Override
public void run() {
while(true)
zs.bowEachOther(ls);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while(true)
ls.bowEachOther(zs);
}
}).start();
}
static class Friend {
private String name;
public Friend(String name) {
this.name = name;
}
public synchronized void bow(Friend friend) {
System.out.println(Thread.currentThread().getName()+":::"+this.name+" bow to " + friend.name);
friend.bowBack(this);//同步中调用其它对象的同步方法可能会发生死锁:相互等待对方释放锁
System.out.println(Thread.currentThread().getName()+":::"+"bow done!");
}
public synchronized void bowBack(Friend friend) {
System.out.println(Thread.currentThread().getName()+":::"+this.name+" bow to " + friend.name);
}
//额外引入一个能让线程排他性访问的方法,这些线程需要争夺同一个锁
public void bowEachOther(Friend friend) {
//使用同一个锁,实现排他性的访问
synchronized (Friend.class) {
bow(friend);
}
}
}
}
B.线程饥饿
当某个线程占有了锁,并且需要执行很长一段时间才释放锁,这就导致其它等待锁释放的线程处于阻塞状态。这种情况下其它线程便处于“饥饿”状态。
C.线程活锁
活锁指的是线程间相互响应时,由于响应结果不正确而导致彼此一直都处于响应状态。
线程并没有阻塞,只是一直在响应而无法恢复到正常的工作中。
线程协作
实际开发中,很多场景都可以归结为生产者-消费者的协作关系。
基本规则:
任务池满的时候,阻塞生产者,直到任务池中有任务被取走;
任务池空的时候,阻塞消费者,直到任务池增加了新的任务;
实现原理:
1. 同步:生产者与消费者使用同一个锁
2. 协作:
wait() 线程判断条件不满足时,等待
notify()/notifyAll() 解除对方的等待
以下示例通过synchronized、while->wait()、notifyAll()模拟生产者-消费者的模型。
实际开发中,不需要自己再去发明轮子了,请使用java.util.concurrent包中的工具类完成需要的功能
package org.thread;
import java.util.concurrent.ThreadLocalRandom;
public class ConsumerProduerDemo {
public static void main(String[] args) {
Messenger messenger = new Messenger();
new Consumer(messenger).start();
new Producer(messenger).start();
}
}
// ---共享资源:message
class Messenger {
private String message;
boolean empty = true;
public synchronized String take() {
while (empty) {
try {
System.out.println("Consumer waiting...");
wait();
} catch (InterruptedException e) {
}
}
empty = true;
notifyAll();
return message;
}
public synchronized void put(String message) {
while (!empty) {
try {
System.out.println("Producer waiting...");
wait();
} catch (InterruptedException e) {}
}
this.message = message;
empty = false;
notifyAll();
}
}
// ---生产者
class Producer implements Runnable {
private Messenger messenger;
public Producer(Messenger messenger) {
this.messenger = messenger;
}
@Override
public void run() {
String[] msgs = { "First msg", "Second msg", "Third msg", "Fouth msg" };
for (int i = 0; i < msgs.length; i++) {
messenger.put(msgs[i]);
try {
Thread.sleep(ThreadLocalRandom.current().nextInt(1000));
} catch (InterruptedException e) {
}
}
messenger.put("Done");
}
public Thread start() {
Thread t = new Thread(this);
t.start();
return t;
}
}
// ---消费者
class Consumer implements Runnable {
private Messenger messenger;
public Consumer(Messenger messenger) {
this.messenger = messenger;
}
@Override
public void run() {
for (String msg = messenger.take(); !"Done".equals(msg); msg = messenger.take()) {
System.out.println("Message take: " + msg);
try {
Thread.sleep(ThreadLocalRandom.current().nextInt(1000));
} catch (InterruptedException e) {
}
}
}
public Thread start() {
Thread t = new Thread(this);
t.start();
return t;
}
}