一、基础概念
1. 并发
多个线程访问同一个对象,并且某些线程还想修改这些对象
2. 线程同步
线程同步是一种等待机制, 通过在访问时加入锁机制 synchronized 来实现,线程同步会导致一些性能问题(性能倒置等)
3. 队列和锁
线程同步需要队列和锁,每个对象都有自己的锁
二、案例
1. 多线程买票问题
线程不安全代码,会出现超卖现象
/**
* @description: 使用多线程,模拟三个窗口同时售票
* @author: Liuwanqing
* @date: 2022-10-14 16:42
*/
public class SellTicket {
public static void main(String[] args) {
SellTickets02 sellTickets02 = new SellTickets02();
// SellTickets01 sellTickets01 = new SellTickets02();
// SellTickets01 sellTickets02 = new SellTickets02();
// SellTickets01 sellTickets03 = new SellTickets02();
// sellTickets01.start();
// sellTickets02.start();
// sellTickets03.start();
// 创建 3 个线程进行买票
Thread thread1 = new Thread(sellTickets02);
Thread thread2 = new Thread(sellTickets02);
Thread thread3 = new Thread(sellTickets02);
thread1.start();
thread2.start();
thread3.start();
}
}
// 使用 Thread 方式
class SellTickets01 extends Thread {
private static int tikectNum = 100; // 多线程共享的 num
@Override
public void run() {
while (true) {
if (tikectNum <= 0) {
System.out.println("票卖完了~~~");
break;
}
try {
Thread.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("窗口 " + Thread.currentThread().getName() + " 售出了一张票,还剩的票数为" + (--tikectNum) );
}
}
}
// 使用实现 Runnable 接口的方式
class SellTickets02 implements Runnable {
private static int tikectNum = 100; // 多线程共享的 num
@Override
public void run() {
while (true) {
if (tikectNum <= 0) {
System.out.println("票卖完了~~~");
break;
}
try {
Thread.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("窗口 " + Thread.currentThread().getName() + " 售出了一张票,还剩的票数为" + (--tikectNum) );
}
}
}
2. 银行取钱问题
/**
* @Author: WanqingLiu
* @Date: 2023/02/02/15:03
* 银行取钱问题
*/
public class Draw {
public static void main(String[] args) {
Account account = new Account("晚晴", 100);
DrawThread wanqing = new DrawThread(account, 10);
DrawThread xinyu = new DrawThread(account, 10);
wanqing.start();
xinyu.start();
}
}
// 银行账户类
class Account {
String name; // 账户名
int nowNum; // 账户余额
public Account(String name, int nowNum) {
this.name = name;
this.nowNum = nowNum;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getNowNum() {
return nowNum;
}
public void setNowNum(int nowNum) {
this.nowNum = nowNum;
}
}
// 取钱线程
class DrawThread extends Thread {
Account account; // 取钱的账户
int drawNum; // 取走的钱
public DrawThread(Account account, int drawNum){
this.account = account;
this.drawNum = drawNum;
}
@Override
public void run() {
if ((account.getNowNum() - drawNum) < 0) {
System.out.println("余额不足");
return;
}
try { // 放大问题的发生性 *
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
account.setNowNum(account.getNowNum() - drawNum);
System.out.println("当前余额为 :" + account.getNowNum());
}
}
3. 线程不安全的集合
ArrayList - 线程不安全
public static void main(String[] args) {
List<String> list = new ArrayList<>();
// 开启 10000 个线程,向 list 中加入 10000 个数据
for (int i=0; i<10000; i++){
// 两个线程覆盖了同一个位置
new Thread(() ->{
list.add(Thread.currentThread().getName());
}).start();
}
// 输出并不为 10000 , 因为两个线程同时加入时,加入的元素覆盖了同一个位置
System.out.println(list.size());
}
三、同步方法
1. synchronized
synchronized 默认锁的是对象本身(this),我们可以通过同步块锁任何对象,锁的对象应该是多线程操作的那个变化的量,如在银行取钱中,我们要锁的是要变化的账户对象
- (1)同步方法
同步方法实现线程安全的买票
package concurrency;
/**
* @description: 使用多线程,模拟三个窗口同时售票
* @author: Liuwanqing
* @date: 2022-10-14 16:42
*/
public class SellTicket {
public static void main(String[] args) {
SellTickets02 sellTickets02 = new SellTickets02();
// 创建 3 个线程进行买票
Thread thread1 = new Thread(sellTickets02);
Thread thread2 = new Thread(sellTickets02);
Thread thread3 = new Thread(sellTickets02);
thread1.start();
thread2.start();
thread3.start();
}
}
// 使用 Thread 方式
class SellTickets01 extends Thread {
private static int tikectNum = 100; // 多线程共享的 num
@Override
public synchronized void run() {
while (true) {
if (tikectNum <= 0) {
System.out.println("票卖完了~~~");
break;
}
try {
Thread.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("窗口 " + Thread.currentThread().getName() + " 售出了一张票,还剩的票数为" + (--tikectNum) );
}
}
}
// 使用实现 Runnable 接口的方式
class SellTickets02 implements Runnable {
private static int tikectNum = 100; // 多线程共享的 num
@Override
public synchronized void run() {
while (true) {
if (tikectNum <= 0) {
System.out.println("票卖完了~~~");
break;
}
try {
Thread.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("窗口 " + Thread.currentThread().getName() + " 售出了一张票,还剩的票数为" + (--tikectNum) );
}
}
}
- (2)同步块
- 同步块解决银行取钱问题
/**
* @Author: WanqingLiu
* @Date: 2023/02/02/15:03
* 银行取钱问题
*/
public class Draw {
public static void main(String[] args) {
Account account = new Account("晚晴", 100);
DrawThread wanqing = new DrawThread(account, 10);
DrawThread xinyu = new DrawThread(account, 10);
wanqing.start();
xinyu.start();
}
}
// 银行账户类
class Account {
String name; // 账户名
int nowNum; // 账户余额
public Account(String name, int nowNum) {
this.name = name;
this.nowNum = nowNum;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getNowNum() {
return nowNum;
}
public void setNowNum(int nowNum) {
this.nowNum = nowNum;
}
}
// 取钱线程
class DrawThread extends Thread {
Account account; // 取钱的账户
int drawNum; // 取走的钱
public DrawThread(Account account, int drawNum){
this.account = account;
this.drawNum = drawNum;
}
@Override
public void run() {
// 锁住我们要操作的 account
synchronized (account) {
if ((account.getNowNum() - drawNum) < 0) {
System.out.println("余额不足");
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
account.setNowNum(account.getNowNum() - drawNum);
System.out.println("当前余额为 :" + account.getNowNum());
}
}
}
- 同步块解决 List 不安全问题
public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<>();
for (int i=0; i<10000; i++){
// 两个线程覆盖了同一个位置
new Thread(() ->{
synchronized (list) {
list.add(Thread.currentThread().getName());
}
}).start();
}
Thread.sleep(1000); // 让主线程睡一会,保证能执行完
System.out.println(list.size());
}
CopyOnWriteArrayList
是 JUC 包提供的线程安全集合
public static void main(String[] args) throws InterruptedException {
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
for (int i=0; i<10000; i++){
// 两个线程覆盖了同一个位置
new Thread(() ->{
list.add(Thread.currentThread().getName());
}).start();
}
Thread.sleep(1000); // 让主线程睡一会,保证能执行完
System.out.println(list.size());
}
四、死锁
死锁描述的是一种多个进程被无限期阻塞,它们中的一个或者多个都在等待某一资源被释放,导致程序不能正常终止的情况 —— 就是线程相互等待对方的资源
/**
* @Author: WanqingLiu
* @Date: 2023/02/02/15:51
* 化妆模拟死锁
*/
public class DeadLock {
public static void main(String[] args) {
Makeup girl1 = new Makeup(0, "晚晴");
Makeup girl2 = new Makeup(1, "婉晴");
girl2.start();
girl1.start();
}
}
// 口红类
class LipStick{
}
// 镜子
class Mirror{
}
// 化妆线程
class Makeup extends Thread {
static LipStick lipStick = new LipStick();
static Mirror mirror = new Mirror();
int choice;
String girlName;
public Makeup(int choice, String girlName){
this.choice = choice;
this.girlName = girlName;
}
@Override
public void run() {
// 化妆操作
try {
makeup();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
private void makeup() throws InterruptedException {
if(choice == 0){ // 口红
synchronized (lipStick) {
System.out.println(this.girlName + "获得口红的锁");
Thread.sleep(1000); // 一秒钟之后,她想获得镜子的锁
synchronized (mirror) {
System.out.println(this.girlName + "获得镜子的锁");
}
}
} else { // 镜子
synchronized (mirror) {
System.out.println(this.girlName + "获得镜子的锁");
Thread.sleep(1000); // 一秒钟之后,她想获得口红的锁
synchronized (lipStick) {
System.out.println(this.girlName + "获得口红的锁");
}
}
}
}
}
五、 Lock 锁
1. 基础概念
Lock
锁是 JDK 5.0
开始提供的更加强大的同步机制,其是一个显式同步锁,同步锁通过 Lock
对象充当( java.util.concurrent.locks
)
ReentrantLock
类实现了 Lock
,其与 synchronized
具有相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是 ReentrantLock
,可以显式加锁、释放锁。
2. 使用 ReentrantLock 可重入锁解决买票问题
从下面的代码中,我们也可以看出 Lock 锁是显式的加锁,显式的释放锁 。
import java.util.concurrent.locks.ReentrantLock;
/**
* @Author: WanqingLiu
* @Date: 2023/02/04/10:18
* 测试Lock锁
*/
public class TestLock {
public static void main(String[] args) {
TestLock2 testLock2 = new TestLock2();
new Thread(testLock2).start();
new Thread(testLock2).start();
new Thread(testLock2).start();
}
}
class TestLock2 implements Runnable {
private static int tikectNum = 100; // 多线程共享的 num
// 定义 lock 锁
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
lock.lock();
if (tikectNum <= 0) {
System.out.println("票卖完了~~~");
break;
}
try {
Thread.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("窗口 " + Thread.currentThread().getName() + " 售出了一张票,还剩的票数为" + (--tikectNum));
} finally {
lock.unlock();
}
}
}
}
3. synchronized 与 Lock 对比
Lock
是显式锁,即需要手动开关锁;synchronized
是隐式锁,即出了作用域锁自动释放Lock
只具有代码块锁,而synchronized
还具有方法锁- 使用
Lock
锁,JVM 花费更少的时间调度线程,性能更好,并且具有更好的扩展性(子类)