线程的深入理解
多线程安全问题
- 当多线程并发访问临界资源时,若破坏原子操作,可能会造成数据不一致。
- 临界资源:共享资源(同一对象),一次仅允许一个线程使用,才可保证其正确性。
- 原子操作:不可分割的多步操作,被视作一个整体,其顺序和步骤不可打乱或缺省。
线程同步
(一)同步方式
1. 同步代码块
synchronized (临界资源对象) { //对临界资源对象加锁
//代码(原子操作)
}
- 每个对象都有一个互斥锁标记,用来分配给线程。
- 只有拥有对象互斥锁标记的线程,才能进入对该对象加锁的同步代码块。
- 把同步代码块锁住,只让一个线程在同步代码块中执行。
- 必须保证多个线程使用的锁对象是同一个。
- 线程退出同步代码块时,会释放相应的互斥锁标记。
/**
* 实现 Runnable 接口实现卖票安全机制
* @author Nigori
* @date 2020/8/2
**/
public class Ticket implements Runnable {
private int ticket = 100;
private Object object = new Object();
@Override
public void run() {
while (true) {
synchronized (this) { // 也可以是object,原因:始终只有一个对象
if (ticket<=0) break;
System.out.println(Thread.currentThread().getId() + ": " + Thread.currentThread().getName() + "卖了第 " + ticket + "张票!");
ticket--;
}
}
}
}
2. 同步方法
//对当前对象(this) 加锁即【 new Ticket() 】
修饰符 synchronized 返回值类型 方法名称(形参列表){
//代码(原子操作)
}
- 只有拥有对象互斥锁标记的线程,才能进入该对象加锁的同步方法中。
- 线程退出同步方法时,会释放相应的互斥锁标记。
public class Ticket implements Runnable {
private int ticket = 100;
private Object object = new Object();
@Override
public void run() {
while (true) {
if (!sale()) {
break;
}
}
}
public synchronized boolean sale() { //锁对象为this 若为静态方法:锁对象为Ticket.class
if (ticket<=0)
return false;
System.out.println(Thread.currentThread().getId() + ": " + Thread.currentThread().getName() + "卖了第 " + ticket + "张票!");
ticket--;
return true;
}
}
3. 锁机制
位置: java.util.concurrent.locks
- JDK 1.5加入,常用方法:
void lock()
获取锁,若锁被占用,则等待boolean tryLock()
常数获取锁,成功返回true
,失败返回false
,不阻塞void unlock()
释放锁
3.1 重入锁
ReentrantLock
类实现 Lock
接口,具有互斥性
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Ticket implements Runnable {
private int ticket = 10000;
private Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
lock.lock(); //加锁
try {
if (ticket <= 0) break;
System.out.println(Thread.currentThread().getName() + "卖了第" + ticket + "张票!");
ticket--;
} finally {
lock.unlock(); //释放锁
}
}
}
}
3.2 读写锁
ReentrantReadWriteLock
类实现 Lock
接口
一种支持一写多读的同步锁,读写分离,可分别分配读锁、写锁。支持多次分配读锁,使多个读操作可以并发执行。
- 互斥规则
- 写——写:互斥,阻塞。
- 读——写:互斥,读阻塞写、写阻塞读。
- 读——读:不互斥、不阻塞。
- 在读操作远远高于写操作的环境中,可在保障线程安全的情况下,提高运行效率。
//import java.util.concurrent.locks.Lock;
**import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class RWLock {
private String str;
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); //读写锁 用时:4013
private ReentrantReadWriteLock.ReadLock readLock = rwl.readLock();
private ReentrantReadWriteLock.WriteLock writeLock = rwl.writeLock();
//private Lock lock = new ReentrantLock(); //互斥锁 用时:20103
//读操作
public String getValue() {
readLock.lock();
//lock.lock();
try {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "读出的值为: " + str);
return str;
} finally {
readLock.unlock();
//lock.unlock();
}
}
//写操作
public void setValue(String str) {
writeLock.lock();
//lock.lock();
try {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "写入的值为: " + str);
this.str = str;
} finally {
writeLock.unlock();
//lock.unlock();
}
}
}
3.3. 死锁
- 当第一个线程拥有A对象锁标记,并等待B对象锁标记,同时第二个线程拥有B对象锁标记,并等待A对象锁标记时,产生死锁。
- 一个线程可以同时拥有多个对象的锁标记,当线程阻塞时,不会释放已经拥有的锁标记,由此可能造成死锁。
@Example:
public class TeamWork {
public static Object thingsA = new Object();
public static Object thingsB = new Object();
}
public class RunnablDeathLockDemo {
public static void main(String[] args) throws InterruptedException {
Runnable personA = new Runnable() {
@Override
public void run() {
synchronized (TeamWork.thingsA) {
System.out.println("A拿到了thingsA");
synchronized (TeamWork.thingsB) {
System.out.println("A拿到了thingsB");
System.out.println("A可以完成整个things");
}
}
}
};
Runnable personB = new Runnable() {
@Override
public void run() {
synchronized (TeamWork.thingsB) {
System.out.println("B拿到了thingsB");
synchronized (TeamWork.thingsA) {
System.out.println("B拿到了thingsA");
System.out.println("B可以完成整个things");
}
}
}
};
Thread pa = new Thread(personA);
Thread pb = new Thread(personB);
pa.start();
Thread.sleep(200); //解决死锁
pb.start();
}
}
(二)线程通信
public class BankCard {
private double money;
private boolean flag; //true:只能取 false:只能存
public synchronized void addMoney(double m) {
while (flag) {
try {
this.wait(); //进入等待队列,同时释放锁和CPU
} catch (InterruptedException e) {
e.printStackTrace();
}
}
money = money + m;
System.out.println(Thread.currentThread().getName() + "存了" + m + "元,余额是:" + money);
flag = true;
this.notifyAll();
}
public synchronized void takeMoney(double m) {
while (!flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
money = money - m;
System.out.println(Thread.currentThread().getName() + "取了" + m + "元,余额是:" + money);
flag = false;
this.notifyAll();
}
}
1. 等待(java.lang.Object
)
public final void wait(long timeout)
- 在其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量前,导致当前线程等待。
2. 通知(java.lang.Object
)
public final native void notify();
- 唤醒在此对象监视器上等待的单个线程。
public final native void notifyAll();
- 唤醒在此对象监视器上等待的所有线程。
(三)Callable接口
public interface Callable<V> {
public V call() throws Exception;
}
- JDK5加入,与
Runnable
接口类似,但是Callable
具有泛型返回值、可以声明异常, 实现之后代表一个线程任务。
public class CallableDemo {
public static void main(String[] args) throws Exception {
//创建Callable对象
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i <= 100; i++) {
sum+=i;
}
return sum;
}
};
//把Callable对象转换成可执行任务
FutureTask<Integer> futureTask = new FutureTask<>(callable);
//创建线程
Thread thread = new Thread(futureTask);
//启动线程
thread.start();
//获取结果(等待call执行完毕才会返回结果)
System.out.println("1~100的求和结果为:" + futureTask.get());;
}
}
(四)Future接口
V get()
以阻塞形式等待Future
中的异步处理结果,即call()
的返回值
public class PoolSumDemo_01 {
public static void main(String[] args) throws Exception {
//创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(1);
//提交任务 Future:表示将要执行完任务的结果
Future<Integer> future = executorService.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i <= 100; i++) {
sum+=i;
}
return sum;
}
});
//获取结果:等待任务执行完毕才会返回
System.out.println("使用Future,一个线程求0~100和结果为:" + future.get());
//关闭线程池
executorService.shutdown();
}
}
(五)线程同步
形容一 次方法调用,同步一旦开始,调用者必须等待该方法返回,才能继续。
(六)线程异步
形容一次方法调用,异步一旦开始,像是一次消息传递,调用者告知之后立刻返回。二者竞争时间片,并发执行。