一、不安全案例
多人抢票问题
package org.example.thread;
/**
* 多人抢票问题
* @author lzhiyun
* @date 2020/6/28 7:44
*/
public class UnsafeThreadDemo {
static class TicketThread implements Runnable {
// 总票数
private int ticketNumber = 10;
// 标志位
private boolean ticketFlag = true;
@Override
public void run() {
while (ticketFlag) {
buyTicket();
}
}
private void buyTicket() {
// 如果票数不够,则退出
if (ticketNumber <= 0) {
ticketFlag = false;
return;
}
// 模拟网络延迟
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 票数足够,则购买
System.out.println(Thread.currentThread().getName() + "购买了第" + (ticketNumber--) + "张票");
}
}
public static void main(String[] args) {
// 创建购票线程
TicketThread ticketThread = new TicketThread();
// 创建购票者
new Thread(ticketThread, "甲").start();
new Thread(ticketThread, "乙").start();
new Thread(ticketThread, "丙").start();
/* 输出结果,出现两次第7张票;出现了第0张票
* 甲购买了第10张票
* 乙购买了第8张票
* 丙购买了第9张票
* 乙购买了第6张票
* 丙购买了第7张票
* 甲购买了第7张票
* 丙购买了第5张票
* 甲购买了第3张票
* 乙购买了第4张票
* 丙购买了第2张票
* 甲购买了第1张票
* 乙购买了第0张票
*/
}
}
银行取钱问题
package org.example.thread;
import lombok.Data;
/**
* 银行取钱问题
* @author lzhiyun
* @date 2020/6/28 7:44
*/
public class UnsafeThreadDemo2 {
// 账户
@Data
static class Account {
// 账户余额
private int money;
// 账户名称
private String name;
public Account(int money, String name) {
this.money = money;
this.name = name;
}
}
// 银行取钱线程
static class BankThread extends Thread {
// 账户信息
private Account account;
// 取钱数
private int withdrawMoney;
// 用户名
private String name;
public BankThread(String name, Account account, int withdrawMoney) {
super(name);
this.account = account;
this.withdrawMoney = withdrawMoney;
}
@Override
public void run() {
// 判断是否能够取钱
if (withdrawMoney > account.getMoney()) {
return;
}
// 模拟网络延迟
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 返回结果
account.setMoney(account.getMoney() - withdrawMoney);
System.out.println(getName() + " 取走了 " + withdrawMoney);
System.out.println(account.getName() + " 账户剩余 " + account.getMoney());
}
}
public static void main(String[] args) {
// 创建账户
Account account = new Account(100, "工商银行");
// 创建取钱用户
new BankThread("张三", account, 50).start();
new BankThread("李四", account, 100).start();
/* 输出结果
* 张三 取走了 50
* 工商银行 账户剩余 50
* 李四 取走了 100
* 工商银行 账户剩余 -50
*/
}
}
集合赋值问题
package org.example.thread;
import java.util.ArrayList;
import java.util.List;
/**
* 集合赋值问题
* @author lzhiyun
* @date 2020/6/28 7:44
*/
public class UnsafeThreadDemo3 {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
new Thread(() -> {
list.add("hello world");
}).start();
}
System.out.println(list.size());
/* 输出结果
* 9994
*/
}
}
二、synchronized
介绍
线程安全问题:多个线程操作同个共享变量时,会因为其他线程的干扰导致数据产生误差,就会出现线程安全问题。
synchronized,能够保证在同一时刻最多只有一个线程执行某个方法或代码块,保证并非的原子性、可见性和有序性。当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放即可。
问题
- 一个线程持有锁会导致其他所有需要此锁的线程挂起;
- 在多线程竞争下,加锁、释放锁会导致较多的上下文切换和调度延时,引起性能问题;
- 如果一个优先级高的线程等待一个优先级低的线程,会导致优先级倒置,引起性能问题。
用法
- synchronized普通同步方法
public synchronized void method(String[] args) {}
锁的是当前实例对象,进入同步代码前要获取当前实例的锁。
- synchronized静态同步方法
public static synchronized void method(String[] args) {}
锁的是当前类的class对象,进入同步代码前要获得当前类对象的锁。
- synchronized同步方法块
synchronized(Object) {...}
锁的是括号里的对象,进入同步代码前要获取给定对象的锁。
对象监视器
- 同步代码块里的Object可以是任何对象,但是推荐使用共享资源作为对象监视器。
- 同步方法无需指定对象监视器,因为同步方法的对象监视器就是this,表示这个实例对象,或者是class对象。
- 对象监视器的执行过程:
- 第一个线程访问,锁定对象监视器,执行其中代码。
- 第二个线程访问,发现对象监视器被锁定,挂起无法访问。
- 第一个线程访问完毕,解锁对象监视器。
- 第二个线程访问,发现对象监视器没有锁,然后锁定并执行。
总结
- 用域控制对”对象“的访问,每个对象对应一把锁,只有获得该锁才能执行,否则线程会阻塞。方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。
- 方法里面需要修改的内容才需要锁,查询的内容不需要,锁的太多,浪费资源,影响效率。
三、Lock
介绍
从JDK5开始,Java提供了通过显示定义同步锁对象来实现同步。
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。
锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
Lock和ReadWriteLock是两大锁的根接口,Lock代表实现类是ReentrantLock(可重入锁),ReadWriteLock(读写锁)的代表实现类是ReentrantReadWriteLock。
方法
- lock(),该方法用于获取锁,如果锁已经被其他线程获取,则进行等待。
- unLock(),该方法用域释放锁,一般将释放锁操作放在finally块中进行。
- tryLock(),该方法用域获取锁并且有返回值,如果获取成功返回true,获取失败返回false。该方法无论如何都会立即返回,在拿不到锁时不会一直在那等待。
- tryLock(long time, TimeUnit unit),与tryLock()方法是类似的,但是多了time(TimeUnit)的等待时间。时间范围内获取到锁则返回true,否则返回false。
Lock lock = new ReentranLock();
lock.lock();
try {
// TODO
} catch(Exception e) {
// TODO
} finally {
lock.unlock(); // 释放锁
}
synchronized与Lock的对比
- Lock是java.util.concurrent.locks包下的一个接口,synchronized是Java的一个关键字,属于内置的语言实现。
- Lock是显式锁(手动获取和释放锁),synchronized是隐式锁,出了作用域自动释放。
- Lock只有代码块锁,synchronized有代码块锁和方法锁。
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)。
- 优先使用顺序:;
Lock > 同步代码块(已经进入了方法体,分配了相应资源) > 同步方法(在方法体之外)
三、死锁
案例
package org.example.thread;
/**
* 死锁demo
* @author lzhiyun
* @date 2020/6/28 8:02
*/
public class DeadLock {
public static void main(String[] args){
// 共享资源obj1
public static String obj1 = "obj1";
// 共享资源obj2
public static String obj2 = "obj2";
// 执行线程LockA
new Thread(new LockA()).start();
// 执行线程LockB
new Thread(new LockB()).start();
}
}
/**
* 线程LockA,先锁住obj1,在锁住obj2
*/
class LockA implements Runnable{
@Override
public void run(){
try{
System.out.println("LockA running");
while(true){
synchronized(DeadLock.obj1){
System.out.println("LockA lock obj1");
Thread.sleep(2000);
synchronized(DeadLock.obj2){
System.out.println("LockA lock obj2");
}
}
}
}catch(Exception e){
e.printStackTrace();
}
}
}
/**
* 线程LockB,先锁住obj2,在锁住obj1
*/
class LockB implements Runnable{
@Override
public void run(){
try{
System.out.println("LockB running");
while(true){
synchronized(DeadLock.obj2){
System.out.println("LockB lock obj2");
Thread.sleep(2000);
synchronized(DeadLock.obj1){
System.out.println("LockB lock obj1");
}
}
}
}catch(Exception e){
e.printStackTrace();
}
}
}
四个必要条件
- 互斥条件:一个资源每次只能被一个进程使用;
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
注意:破坏其中的任意一个或者多个条件,就可以避免死锁的发生。