【Java并发编程】线程安全问题、线程同步、单例模式(懒汉式)改进、细节、几个常用类的细节、死锁(Deadlock)、线程间通讯-生产者消费者模型、ReentrantLock(可重入锁)、线程池
并发编程
文章目录
- 并发编程
-
- 07_线程安全问题
-
- 线程安全问题 – 错误示例
- 解决方案 - 线程同步
- 线程同步 - 同步语句
- 线程同步 - 同步方法
- 08_单例模式(懒汉式)改进、细节
- 几个常用类的细节
- 09_死锁(Deadlock)
-
- 死锁示例1
- 死锁示例2
- 10_线程间通讯
-
- 线程间通信 - 生产者消费者模型
- 11_ReentrantLock(可重入锁)
-
- `lock`、`trylock`
- ReentrantLock 在卖票示例中的使用
- ReentrantLock – `tryLock`使用注意
- 12_线程池(Thread Pool)
07_线程安全问题
多个线程可能会共享(访问)同一个资源
- 比如访问同一个对象、同一个变量、同一个文件
当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题,称为线程安全问题。
什么情况下会出现线程安全问题?
- 多个线程共享同一个资源
- 且至少有一个线程正在进行 写(write) 的操作
例如:存钱取钱过程
卖票过程
线程安全问题 – 错误示例
编写一个站台类:
public class Station implements Runnable {private int tickets = 100;/** * 卖一张票 */public boolean saleTicket(){if(tickets < 1) return false; // 票卖完了,不卖了tickets--;String name = Thread.currentThread().getName();System.out.println(name + "卖了1张票,还剩" + tickets + "张");return tickets > 0;}@Overridepublic void run() {while(saleTicket()); // 只要能卖票就一只卖}}
public static void main(String[] args) {Station station = new Station();for (int i = 1; i <= 4; i++) {Thread thread = new Thread(station);thread.setName("" + i);thread.start();}}
会发现结果不是我们想要的,票数乱七八糟
....2卖了1张票,还剩47张2卖了1张票,还剩45张2卖了1张票,还剩44张2卖了1张票,还剩43张2卖了1张票,还剩42张2卖了1张票,还剩41张2卖了1张票,还剩40张2卖了1张票,还剩39张2卖了1张票,还剩38张2卖了1张票,还剩37张2卖了1张票,还剩36张1卖了1张票,还剩47张1卖了1张票,还剩34张1卖了1张票,还剩33张1卖了1张票,还剩32张1卖了1张票,还剩31张1卖了1张票,还剩30张4卖了1张票,还剩46张4卖了1张票,还剩28张4卖了1张票,还剩27张4卖了1张票,还剩26张4卖了1张票,还剩25张4卖了1张票,还剩24张4卖了1张票,还剩23张4卖了1张票,还剩22张4卖了1张票,还剩21张4卖了1张票,还剩20张4卖了1张票,还剩19张4卖了1张票,还剩18张4卖了1张票,还剩17张3卖了1张票,还剩47张3卖了1张票,还剩15张3卖了1张票,还剩14张4卖了1张票,还剩16张4卖了1张票,还剩12张4卖了1张票,还剩11张4卖了1张票,还剩10张4卖了1张票,还剩9张4卖了1张票,还剩8张4卖了1张票,还剩7张1卖了1张票,还剩29张1卖了1张票,还剩5张1卖了1张票,还剩4张1卖了1张票,还剩3张1卖了1张票,还剩2张1卖了1张票,还剩1张1卖了1张票,还剩0张2卖了1张票,还剩35张4卖了1张票,还剩6张3卖了1张票,还剩13张
问题分析:
解决方案 - 线程同步
可以使用线程同步技术来解决线程安全问题
- 同步语句(Synchronized Statement)
- 同步方法(Synchronized Method)
线程同步 - 同步语句
将上面错误示例的代码修改成如下,则正确了
public boolean saleTicket(){synchronized (this) {if(tickets < 1) return false;tickets--;String name = Thread.currentThread().getName();System.out.println(name + "卖了1张票,还剩" + tickets + "张");return tickets > 0;}}
.....1卖了1张票,还剩49张1卖了1张票,还剩48张1卖了1张票,还剩47张1卖了1张票,还剩46张1卖了1张票,还剩45张1卖了1张票,还剩44张4卖了1张票,还剩43张4卖了1张票,还剩42张4卖了1张票,还剩41张4卖了1张票,还剩40张4卖了1张票,还剩39张3卖了1张票,还剩38张3卖了1张票,还剩37张3卖了1张票,还剩36张3卖了1张票,还剩35张3卖了1张票,还剩34张3卖了1张票,还剩33张3卖了1张票,还剩32张3卖了1张票,还剩31张3卖了1张票,还剩30张3卖了1张票,还剩29张3卖了1张票,还剩28张3卖了1张票,还剩27张3卖了1张票,还剩26张3卖了1张票,还剩25张3卖了1张票,还剩24张3卖了1张票,还剩23张3卖了1张票,还剩22张3卖了1张票,还剩21张3卖了1张票,还剩20张3卖了1张票,还剩19张3卖了1张票,还剩18张2卖了1张票,还剩17张2卖了1张票,还剩16张2卖了1张票,还剩15张2卖了1张票,还剩14张2卖了1张票,还剩13张2卖了1张票,还剩12张2卖了1张票,还剩11张2卖了1张票,还剩10张2卖了1张票,还剩9张2卖了1张票,还剩8张2卖了1张票,还剩7张2卖了1张票,还剩6张2卖了1张票,还剩5张2卖了1张票,还剩4张2卖了1张票,还剩3张2卖了1张票,还剩2张2卖了1张票,还剩1张2卖了1张票,还剩0张
synchronized(obj)
的原理:
- 每个对象都有一个与它相关的内部锁(intrinsic lock)或者叫监视器锁(monitor lock)
- 第一个执行到同步语句的线程可以获得 obj 的内部锁,在执行完同步语句中的代码后释放此锁
- 只要一个线程持有了内部锁,那么其它线程在同一时刻将无法再获得此锁
当它们试图获取此锁时,将会进入BLOCKED
状态。
多个线程访问同一个 synchronized(obj)
语句时
- obj 必须是同一个对象,才能起到同步的作用。
线程同步 - 同步方法
public synchronized boolean saleTicket(){if(tickets < 1) return false;tickets--;String name = Thread.currentThread().getName();System.out.println(name + "卖了1张票,还剩" + tickets + "张");return tickets > 0;}
synchronized
不能修饰构造方法
同步方法的本质
- 实例方法:
synchronized (this)
- 静态方法:
synchronized (Class对象)
同步语句比同步方法更灵活一点
- 同步语句可以精确控制需要加锁的代码范围
使用了线程同步技术后
- 虽然解决了线程安全问题,但是降低了程序的执行效率
- 所以在真正有必要的时候,才使用线程同步技术
08_单例模式(懒汉式)改进、细节
public class Rocket {private static Rocket instance = null;private Rocket() {}public static synchronized Rocket getInstance(){if(instance == null){instance = new Rocket();}return instance;}}
几个常用类的细节
动态数组:
ArrayList
:非线程安全Vector
:线程安全
动态字符串:
StringBuilder
:非线程安全StringBuffer
:线程安全
映射(字典):
HashMap
:非线程安全Hashtable
:线程安全
09_死锁(Deadlock)
什么是死锁?
- 两个或者多个线程永远阻塞,相互等待对方的锁
死锁示例1
以下代码会造成死锁:
- 第一个进程获得了 “1” 的同步锁,又想要获得 “2” 的同步锁
- 第二个进程获得了 “2” 的同步锁,想要获得进程 “1” 的同步锁
- 第一个进程和第二个进程互相等待对方释放,谁也不会主动释放,造成了死锁
public static void main(String[] args) {new Thread(() -> {synchronized ("1") { // 进程1获得了 "1" 的同步锁System.out.println("1 - 1");try{Thread.sleep(100);} catch (Exception e) {e.printStackTrace();}synchronized ("2") { // 进程1想要获得 "2" 的同步锁System.out.println("1 - 2");}}}).start();;new Thread(() -> {synchronized ("2") { // 进程2获得了 "2" 的同步锁System.out.println("2 - 1");try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}synchronized ("1") { // 进程2想要获得 "1" 的同步锁System.out.println("2 - 2");}}}).start();;}
死锁示例2
class Person{private String name;public Person(String name){this.name = name;}public synchronized void hello(Person p){System.out.format("[%s] hello to [%s]%n", name, p.name);p.smile(this);}public synchronized void smile(Person p){System.out.format("[%s] smile to [%s]%n", name, p.name);}}
public class Deadlock {public static void main(String[] args) {Person jack = new Person("Jack");Person rose = new Person("Rose");new Thread(() -> {jack.hello(rose);}).start();;new Thread(() -> {rose.hello(jack);}).start();;}}
10_线程间通讯
可以使用 Object.wait
、Object.notify
、Object.notifyAll
方法实现线程之间的通信
若想在线程 A 中成功调用 obj.wait
、obj.notify
、obj.notifyAll
方法
- 线程 A 必须要持有 obj 的内部锁
obj.wait
:释放 obj 的内部锁,当前线程进入WAITING
或 TIMED_WAITING
状态
obj.notifyAll
:唤醒所有因为 obj.wait
进入WAITING
或 TIMED_WAITING
状态的线程
obj.notify
:随机唤醒 1 个因为 obj.wait
进入WAITING
或 TIMED_WAITING
状态的线程
线程间通信 - 生产者消费者模型
- Drop:食品
- Consumer`:消费者
- Producer:生产者
- main:测试类
/** * @author yusael */public class Drop {private String food;// empty为true代表:消费者需要等待生产者生产食品// empty为false代表:食品生产完毕,生产者要等待消费者消化完食品private boolean empty = true;/** * get方法在消费者线程中执行 * @return */public synchronized String get(){while(empty){try {wait();} catch (InterruptedException e) {}}empty = true;notifyAll();return food;}/** * add方法在生产者线程中执行 * @param food */public synchronized void add(String food){while(!empty){try {wait();} catch (InterruptedException e) {}}empty = false;this.food = food;notifyAll();}}
/** * 生产者 * @author yusael */public class Consumer implements Runnable {private Drop drop;public Consumer(Drop drop) {this.drop = drop;}@Overridepublic void run() {String food = null;while((food = drop.get()) != null){System.out.format("消费者接收到生产者生产的食物:%s%n", food);try {Thread.sleep(1000); // 消费者吃食物2秒} catch (InterruptedException e) {}}}}
/** * 消费者 * @author yusael */public class Producer implements Runnable {private Drop drop;public Producer(Drop drop) {this.drop = drop;}@Overridepublic void run() {String foods[] = {"beef", "bread", "apple", "cookie"};for (int i = 0; i < foods.length; i++) {try {Thread.sleep(1000); // 生产者生产食物2秒} catch (InterruptedException e) {}// 将foods[i]传递给消费者drop.add(foods[i]);}// 告诉消费者:不会再生产任何东西了drop.add(null);}}
package com.yu;public class Main {public static void main(String[] args) {Drop drop = new Drop();(new Thread(new Consumer(drop))).start(); // 开启消费者线程(new Thread(new Producer(drop))).start(); // 开启生产者线程}}
消费者接收到生产者生产的食物:beef消费者接收到生产者生产的食物:bread消费者接收到生产者生产的食物:apple消费者接收到生产者生产的食物:cookie
11_ReentrantLock(可重入锁)
ReentrantLock ,译为“可重入锁”,也被称为“递归锁”
- 类的全名是:
java.util.concurrent.locks.ReentrantLock
- 具有跟同步语句、同步方法(
synchronized
)一样的一些基本功能,但功能更加强大
什么是可重入(rerntrant)?
- 同一个线程可以重复获取同一个锁
- 其实
synchronized
也是可重入的
public static void main(String[] args) {synchronized ("1") {synchronized("1"){System.out.println("synchronized是可重入锁");}}}
该例获取了两次 “1” 的内部锁,仍然可以执行,在有的语言中是不允许这样,那就不是可重入锁。
lock
、trylock
ReentrantLock.lock
:获取此锁
- 如果此锁没有被另一个线程持有,则将锁的持有计数设为 1,并且此方法立即返回
- 如果当前线程已经持有此锁,则将锁的持有计数加 1,并且此方法立即返回
- 如果此锁被另一个线程持有,并且在获得锁之前,此线程将一直处于休眠状态(相当于
wait
),此时锁的持有计数被设为 1
ReentrantLock.tryLock
:仅在锁未被其他线程持有的情况下,才获取此锁
- 如果此锁没有被另一个线程持有,则将锁的持有计数设为 1,并且此方法立即返回 true
- 如果当前线程已经持有此锁,则将锁的持有计数加 1,并且此方法立即返回 true
- 如果此锁被另一个线程持有,则此方法立即返回 false
ReentrantLock.unlock
:尝试释放此锁
- 如果当前线程持有此锁,则将持有计数减 1
- 如果持有计数现在为 0,则释放此锁
- 如果当前线程没有持有此锁,则抛出
java.lang.IllegalMonitorStateException
ReentrantLock.isLocked
:查看此锁是否被任意线程持有
ReentrantLock 在卖票示例中的使用
import java.util.concurrent.locks.ReentrantLock;public class Station implements Runnable {private int tickets = 50;// ReentrantLock lock = new ReentrantLock(); // 两个都行Lock lock = new ReentrantLock();/** * 卖一张票 */public boolean saleTicket(){lock.lock();try{if(tickets < 1) return false;tickets--;String name = Thread.currentThread().getName();System.out.println(name + "卖了1张票,还剩" + tickets + "张");return tickets > 0;}finally {lock.unlock();}}@Overridepublic void run() {while(saleTicket());}}
ReentrantLock – tryLock
使用注意
Lock lock = new ReentrantLock();new Thread(() -> {try {lock.lock();System.out.println("1");Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}}).start();
Lock lock = new ReentrantLock();new Thread(() -> {boolean locked = false;try{locked = lock.tryLock();System.out.println("2");} finally {if(locked)lock.unlock();}}).start();
12_线程池(Thread Pool)
线程对象占用大量内存,在大型应用程序中,频繁地创建和销毁线程对象会产生大量内存管理开销。
使用线程池可以最大程度地减少线程创建、销毁所带来的开销。
线程池由 工作线程(Worker Thread) 组成
- 普通线程:执行完一个任务后,生命周期就结束了。
- 工作线程:可以执行多个任务(任务没来就一直等,任务来了就干活);
先将任务添加到队列(Queue)中,再从队列中取出任务提交到池中。
常用的线程池类型是固定线程池(Fixed Thread Pool)
- 具有固定数量的正在运行的线程
线程池简单使用:
public static void main(String[] args) {// 创建拥有5条工作线程的固定线程池ExecutorService pool = Executors.newFixedThreadPool(5);// 执行任务pool.execute(() -> {// Thread[pool-1-thread-1,5,main]System.out.println(Thread.currentThread());});pool.execute(() -> {// Thread[pool-1-thread-2,5,main]System.out.println(Thread.currentThread());});pool.execute(() -> {// Thread[pool-1-thread-3,5,main]System.out.println(Thread.currentThread());});// 关闭线程池pool.shutdown();}