一、线程同步机制
1. 并发
- 多个线程操作同一个资源
2. 线程同步
处理多线程问题时,多个线程访问同一个对象,此时就需要线程同步,线程同步就是一个等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕,下一个线程再使用。
3. 队列和锁
经典的例子:多个人想要上一个厕所,为了大家都能有序的上厕所,大家开始排队,形成了等待池。但是为了进入测速的人更安全,于时进入后需要锁好厕所门,这样排队的人就进不来了。
在访问资源时加入锁**synchronized
**,当一个线程获得对象的排他锁,独占资源,其他线程必须等待,使用后释放锁即可。存在以下问题:
- 一个线程持有锁会导致其他所有需要此锁的线程挂起
- 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题
- 如果一个高优先级线程等待一个低优先级线程释放锁,会导致优先级倒置,引起性能问题
二、同步方法
1. Synchronized关键字
每个对象都有一把锁,给方法加了该关键字,都必须获得调用该方法的对象的锁才能执行,否则线程阻塞,方法一旦执行就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获取这个锁。
(1) 同步方法
public synchronized void method(int args) {}
线程不安全示例:
/**
* @ClassName SynchronizedTest
* @Description 线程不安全示例
* @Author wangwk-a
* @Date 2022/1/2 17:12
* @Version 1.0
*/
public class SynchronizedTest {
public static void main(String[] args) {
BuyTicket buyTicket = new BuyTicket();
Thread th1 = new Thread(buyTicket, "小明");
Thread th2 = new Thread(buyTicket, "小红");
Thread th3 = new Thread(buyTicket, "小杨");
Thread th4 = new Thread(buyTicket, "小王");
th1.start();
th2.start();
th3.start();
th4.start();
}
}
class BuyTicket implements Runnable {
private int ticketNums = 10;
boolean flag = true;
@Override
public void run() {
while (flag) {
try {
bug();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void bug() throws InterruptedException {
if (ticketNums <= 0) {
flag = false;
return;
}
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + "拿到" + ticketNums-- + "张票");
}
}
小明拿到10张票
小杨拿到9张票
小红拿到9张票
小王拿到9张票
小明拿到8张票
小杨拿到6张票
小红拿到6张票
小王拿到7张票
小杨拿到4张票
小王拿到3张票
小明拿到5张票
小红拿到5张票
小王拿到2张票
小红拿到1张票
小明拿到1张票
小杨拿到2张票
修改为同步方法
private synchronized void bug() throws InterruptedException {}
小红拿到10张票
小红拿到9张票
小红拿到8张票
小红拿到7张票
小红拿到6张票
小红拿到5张票
小王拿到4张票
小王拿到3张票
小王拿到2张票
小王拿到1张票
缺陷:若将大的方法声明为
synchronized
将会影响效率
(2) 同步块
Obj
称为同步监视器,可以是任何对象,但是推荐使用共享资源- 同步方法中无需指定同步监视器,因为同步方法的同步检视器是
this
,就是i对象本身,或者class
- 同步监视器执行过程:
- 第一个线程访问,锁定同步监视器,执行其中代码
- 第二个线程访问,发现同步监视器被锁定,无法访问
- 第一个线程访问完毕,解锁同步监视器
- 第二个线程访问,发现同步监视器没有锁,然后锁定并访问
synchronized(Obj) {
// 对同步资源获取
}
不安全示例:
/**
* @ClassName SyncBlockTest
* @Description 银行取钱示例--同步块
* @Author wangwk-a
* @Date 2022/1/2 17:30
* @Version 1.0
*/
public class SyncBlockTest {
public static void main(String[] args) {
Account account = new Account("结婚基金", 100);
Drawing you = new Drawing(account, 50, "你");
Drawing girlFriend = new Drawing(account, 100, "girlFriend");
you.start();
girlFriend.start();
}
}
class Account {
/**
* 卡名
*/
String name;
/**
* 余额
*/
int money;
public Account(String name, int money) {
this.name = name;
this.money = money;
}
}
class Drawing extends Thread {
/**
* 账户
*/
Account account;
/**
* 取了多少钱
*/
int getMoney;
/**
* 现在手里有多少钱
*/
int nowMoney;
public Drawing(Account account, int getMoney, String name) {
super(name);
this.account = account;
this.getMoney = getMoney;
}
/**
* 取钱
*/
@Override
public void run() {
if (account.money - getMoney < 0) {
System.out.println(Thread.currentThread().getName() + "钱不够了,取不了");
}
// 模拟网络延时
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 可以取钱
account.money -= getMoney;
// 你手里的钱
nowMoney += getMoney;
System.out.println(account.name + "余额为:" + account.money);
System.out.println(Thread.currentThread().getName() + "手里的钱为:" + nowMoney);
}
}
结婚基金余额为:0
girlFriend手里的钱为:100
结婚基金余额为:-50
你手里的钱为:50
加锁后:
@Override
public void run() {
synchronized (account) {
if (account.money - getMoney < 0) {
System.out.println(Thread.currentThread().getName() + "钱不够了,取不了");
return;
}
// 模拟网络延时
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 可以取钱
account.money -= getMoney;
// 你手里的钱
nowMoney += getMoney;
System.out.println(account.name + "余额为:" + account.money);
System.out.println(Thread.currentThread().getName() + "手里的钱为:" + nowMoney);
}
}
结婚基金余额为:50
你手里的钱为:50
girlFriend钱不够了,取不了
2. JUC(Java Util Concurrent)
Java并发包,有很多封装好的类是线程安全的,示例使用**CopyOnWriteArrayList
**
import java.util.concurrent.CopyOnWriteArrayList;
/**
* @ClassName JUCTest
* @Description 测试JUC安全类型的集合
* @Author wangwk-a
* @Date 2022/1/2 17:50
* @Version 1.0
*/
public class JUCTest {
public static final int THREAD_LOOP = 10000;
public static void main(String[] args) {
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>();
for (int i = 0; i < THREAD_LOOP; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size());
}
}
10000
点开**CopyOnWriteArrayList
**可以看到:
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private static final long serialVersionUID = 8673264195747942595L;
/** The lock protecting all mutators */
final transient ReentrantLock lock = new ReentrantLock();
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
// ...
}
- 这个array使用关键字
transient
和volatile
来修饰,表示是可序列化和保证是唯一的 - 这里面出现了一个锁
ReentrantLock
是可重入锁
3. 死锁
多线程各自占有一些共享资源,并且相互等待其他线程占有的资源才能运行,而导致多个线程都在等待对方释放资源,都停止执行的情形。
/**
* @ClassName DeadLock
* @Description 死锁测试
* @Author wangwk-a
* @Date 2022/1/2 18:05
* @Version 1.0
*/
public class DeadLock {
public static void main(String[] args) {
MakeUp g1 = new MakeUp(0, "灰姑娘");
MakeUp g2 = new MakeUp(1, "白雪公主");
g1.start();
g2.start();
}
}
/**
* 口红
*/
class Lipstick {
}
/**
* 镜子
*/
class Mirror {
}
/**
* 化妆类
*/
class MakeUp extends Thread {
/**
* 需要的资源只有一份,用static保障
*/
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) {
e.printStackTrace();
}
}
/**
* 化妆的方法,互相持有对方的锁
*/
private void makeup() throws InterruptedException {
if (choice == 0) {
synchronized (lipstick) {
// 获取口红的锁
System.out.println(this.girlName + "获得口红");
Thread.sleep(1000);
synchronized (mirror) {
// 1s后想获得镜子
System.out.println(this.girlName + "获得镜子");
}
}
} else {
synchronized (mirror) {
// 获取镜子的锁
System.out.println(this.girlName + "获得镜子");
Thread.sleep(2000);
synchronized (lipstick) {
// 2s后想获得口红
System.out.println(this.girlName + "获得口红");
}
}
}
}
}
灰姑娘获得口红
白雪公主获得镜子
// 程序并没有结束...
解决:不要让她们两个“拿着碗里的,看着锅里的”,让她们用完先释放,然后再获取别的锁
private void makeup() throws InterruptedException {
if (choice == 0) {
synchronized (lipstick) {
// 获取口红的锁
System.out.println(this.girlName + "获得口红");
Thread.sleep(1000);
}
synchronized (mirror) {
// 1s后想获得镜子
System.out.println(this.girlName + "获得镜子");
}
} else {
synchronized (mirror) {
// 获取镜子的锁
System.out.println(this.girlName + "获得镜子");
Thread.sleep(2000);
}
synchronized (lipstick) {
// 2s后想获得口红
System.out.println(this.girlName + "获得口红");
}
}
}
灰姑娘获得口红
白雪公主获得镜子
白雪公主获得口红
灰姑娘获得镜子
死锁产生的四个条件:
- 互斥:一个资源每次只能被一个进程使用
- 请求与保持:线程因请求资源而阻塞时,对已获取的资源保持不放
- 不剥夺:线程已获得的资源,在未使用完之前,不能被剥夺
- 循环等待:若干线程形成环的循环等待资源关系
4. Lock
锁
- 从
JDK5.0
开始,Java提供更强大的线程同步机制,通过显式定义同步锁对象来实现同步 - **
java.util.concurrent.locks.Lock
**接口是控制多个线程对共享资源访问的工具。提供对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前要先获得锁 - **
ReentrantLock
**实现了Lock
,它拥有与synchronized
相同的并发性和内存语义,在实现线程安全的控制中常用,可以显示加锁、释放锁
import java.util.concurrent.locks.ReentrantLock;
/**
* @ClassName ReentrantLockTest
* @Description 可重入锁测试
* @Author wangwk-a
* @Date 2022/1/2 18:27
* @Version 1.0
*/
public class ReentrantLockTest {
public static void main(String[] args) {
MyReentrantLock myReentrantLock = new MyReentrantLock();
new Thread(myReentrantLock).start();
new Thread(myReentrantLock).start();
new Thread(myReentrantLock).start();
}
}
class MyReentrantLock implements Runnable{
int ticketNums = 10;
/**
* 定义可重入锁
*/
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
// 加锁
lock.lock();
try {
if (ticketNums > 0) {
try {
Thread.sleep(1000);
System.out.println(ticketNums--);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
} finally {
// 解锁
lock.unlock();
}
}
}
}
10
9
8
7
6
5
4
3
2
1
Process finished with exit code 0
5. synchronized
与Lock
对比
Lock
是显式锁,synchronized
是隐式锁,出了作用域自动释放Lock
只有代码块锁,synchronized
有代码块和方法锁- 使用
Lock
锁,JVM
将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类) - 优先级顺序:
Lock
>同步代码块
>同步方法