1 概念介绍
并发:同一个对象被多个线程同时操作
📌 线程同步
现实生活中,我们会遇到“同一个资源,多个人都想使用”的问题,比如,食堂排队打饭,每个人都想吃饭,最天然的解决办法就是,排队,一个个来。
处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象这时候我们就需要线程同步,线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程再使用。
📌 队列和锁
队列:队列等同于对象的等待池,各线程于队列中进行等待;
锁:保证队列的安全,保证同一时间只有一个线程操作该对象。
📌 Synchronized 方法/同步方法
Sybchronized 机制控制对对象的访问,每个对象对应一把锁,每个 synchronized 方法都必须获取调用方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,知道方法返回才释放锁,后面被阻塞的线程才能获得锁,继续执行;
存在以下问题:
一个线程持有锁会导致其他所有需要此锁的线程挂起;
在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题;
如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题。
2 线程不安全及解决
2.1 同步方法
📌 要点
由于我们可以通过private关键字来保证数据对象只能被方法访问,所以我们只需要针对方法提出一套机制,这套机制就是synchronized关键字,它包括两种用法:synchronized方法 和synchronized块.
同步方法: public synchronized void method(int args)
同步代码块:synchronized(obj){}
Obj 称之为同步监视器,obj 可以是任何对象,但是推荐使用共享资源作为同步监视器,同步方法中无需指定同步监视器,因为同步方法的同步监视器就是 this,这个对象本身;
同步监视器的执行过程:
第一个线程访问,锁定同步监视器,执行其中的代码
第二个线程访问,发现同步监视器被锁定,无法访问
第一个线程访问完毕,解锁同步监视器
第二个线程访问,发现同步监视器没有锁,然后锁定并访问;
2.1 多人购票
📌 不安全代码
public class UnsafeBuyTicket{
public static void main(String[] args) {
BuyTicket buyTicket = new BuyTicket();
new Thread(buyTicket, "A").start();
new Thread(buyTicket, "B").start();
new Thread(buyTicket, "C").start();
}
}
class BuyTicket implements Runnable{
//票数
private int ticketNums= 10;
//外部停止方式
boolean flag = true;
@Override
public void run() {
while (flag){
buy();
}
}
private void buy(){
//判断是否有票
if (ticketNums <=0){
flag = false;
return;
}
System.out.println(Thread.currentThread().getName()+"买到了第"+ticketNums--+"张票;");
}
}
控制台输出:
📢 可以看到输出混乱,C和A拿到了同一张票,甚至有时会出现负数,这就是线程不安全。
这是因为同一时刻,两个线程进行统一操作,内存控制不当会导致数据不一致
📌 同步方法
给buy方法加上了锁~
控制台输出:
📢 可以看到很有顺序。
2.2 银行取钱
📌 不安全代码
public class UnsafeBank {
public static void main(String[] args) {
Account account = new Account(100,"建设银行共同");
new Thread(new Drawing(account, 50),"A").start();
new Thread(new Drawing(account, 100),"B").start();
}
}
//1.创建账户信息
class Account {
//余额
int money;
//卡名
String name;
public Account(int money,String name){
this.money = money;
this.name = name;
}
}
class Drawing implements Runnable{
Account account;
//取出的钱
int drawingMoney;
//现有的钱
int nowMoney;
public Drawing(Account account, int drawingMoney){
this.account = account;
this.drawingMoney = drawingMoney;
}
@Override
public void run() {
//判断有没有钱
if (account.money - drawingMoney < 0){
System.out.println(Thread.currentThread().getName()+"操作:"+account.name+"账户没有那么多钱!");
return;
}
//延时,保证两个线程都能抵达这
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//卡内余额 = 余额 - 取出的钱
account.money = account.money - drawingMoney;
//现有的钱
nowMoney = nowMoney + drawingMoney;
System.out.println(Thread.currentThread().getName()+"操作:"+account.name+"账户余额为"+account.money);
System.out.println(Thread.currentThread().getName()+"手里的钱为"+nowMoney);
}
}
控制台输出:
📢 可以看到两个线程都取了钱,超出了限制条件(不够取的话不能取),线程不安全!
📌 同步代码块
因为同步方法默认是锁this对象,但是这里需要锁这个账户,因此同步代码块,记住锁的对象是变化的量即可。
控制台输出:
📢 上锁成功
2.3 ArrayList
📌 不安全代码
public class UnsafeArrayList {
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
new Thread(()-> arrayList.add(Thread.currentThread().getName())).start();
}
System.out.println(arrayList.size());
}
}
控制台输出:
📢 与10000对不上,说明线程名称加到集合中去的时候有冲突,也是线程不安全。
📌 同步代码块
在lambda表达式中给ArrayList对象添加锁~
控制台输出:
📢 上锁成功
2.4 线程安全的集合
这是Juc里边的集合,自带线程安全功能,底层也是用了锁。
import java.util.concurrent.CopyOnWriteArrayList;
public class JucList {
public static void main(String[] args) {
CopyOnWriteArrayList<String> list= new CopyOnWriteArrayList<>();
for (int i = 0; i < 10000; i++) {
new Thread(()->list.add(Thread.currentThread().getName())).start();
}
//增加延时,方式main线程先结束,不然输出不准确
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size());
}
}
3 死锁和Lock锁
📌 要点
多个线程各自占有一些共享资源,并且相互等待其他线程占用的资源才能运行,而导致两个或两个以上的线程都在等待对方释放资源,都停止执行的情形;某一个同步块同时拥有两个对象以上的锁,就有可能发生死锁
📌 产生死锁的必要条件
互斥条件:一个线程只能被一个人使用
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
不剥夺条件: 进程已获得的资源,在未使用完之前,不能剥夺;
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源的关系
3.1 死锁
简单来说,就是A线程拿到一个口号,B线程拿到一个镜子,然后他们都想再拿到对方的东西,但是无法成功,因为口号和镜子都被加锁了,只有一个线程能用。
//死锁:多个线程互相抱着对方需要的资源,然后形成僵持
public class DeadLock {
public static void main(String[] args) {
Thread t1 = new Thread(new MakeUp(0,"A"));
Thread t2 = new Thread(new MakeUp(1,"B"));
t1.start();
t2.start();
}
}
//口红
class Lipstick{
}
//镜子
class Mirror{
}
class MakeUp implements Runnable{
//需要的资源只有一份
static Lipstick lipstick = new Lipstick();
static Mirror mirror = new Mirror();
int choice;//选择
String name;//使用人
public MakeUp(int choice, String name) {
this.choice = choice;
this.name = name;
}
@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.name+"获取口红的锁");
Thread.sleep(1000);
synchronized (mirror){//一秒钟后获得镜子
System.out.println(this.name+"获取镜子的锁");
}
}
}else{
synchronized (mirror){
System.out.println(this.name+"获取镜子的锁");
Thread.sleep(1000);
synchronized (lipstick){//一秒钟后获得口红
System.out.println(this.name+"获取口红的锁");
}
}
}
}
}
控制台输出:
📢 程序无法结束,这就是死锁。
📌 解决死锁
把同步代码块(上锁的)分开,这样锁就能得到释放。
3.2 Lock锁
📌 要点
Java 提供了更强大的线程同步机制—通过显式定义同步锁对象来实现同步;同步锁使用 Lock 对象充当;
Lock 接口时控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对 Lock 对象加锁,线程开始访问共享资源之前应先获得 Lokc 对象;
ReentrantLock 类(可重用锁)实现了 Lock, 他拥有 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的时 ReentrantLock,可以显示枷锁释放锁;
📌 未加Lock锁时
public class TestLock {
public static void main(String[] args) {
BuyTicket buyTicket = new BuyTicket();
new Thread(buyTicket).start();
new Thread(buyTicket).start();
new Thread(buyTicket).start();
}
}
class BuyTicket implements Runnable{
int ticketNums=10;
@Override
public void run() {
while (true){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(ticketNums>0){
System.out.println(Thread.currentThread().getName()+"买到了票"+ticketNums--);
}else {
break;
}
}
}
}
控制台输出:
📌 加Lock锁后
需要先定义ReentrantLock,然后再try{}finally{}中分别加锁和解锁。
public class TestLock {
public static void main(String[] args) {
BuyTicket buyTicket = new BuyTicket();
new Thread(buyTicket).start();
new Thread(buyTicket).start();
new Thread(buyTicket).start();
}
}
class BuyTicket implements Runnable{
int ticketNums=10;
//定义lock锁
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
lock.lock();//加锁
if(ticketNums>0){
System.out.println(Thread.currentThread().getName()+"买到了票"+ticketNums--);
}else {
break;
}
} finally {
lock.unlock();//解锁
}
}
}
}
控制台输出:
3.3 synchronized和Lock对比
📌 要点
Lock是显式锁(手动开启和关闭锁,别忘记关闭锁)synchronized是隐式锁,出了作用域自动释放
Lock只有代码块锁,synchronized有代码块锁和方法锁
使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
优先使用顺序:Lock>同步代码块(已经进入了方法体,分配了相应资源)>同步方法(在方法体之外)