一、概述
什么是线程安全/不安全?
如果只有一个线程才可以操作此数据,则必是线程安全。如果有多个线程操作此数据,则此数据是共享数据。如果不考虑同步(加锁)机制,会存在线程安全问题。
二、举例
假如现在A、B二人同时进行车票购买,总共有100张车票,A、B各买一张票。如果是线程安全,那么应该是A、B各买一张票后,剩余 98 张票。
线程安全:指多个线程在执行同一段代码的时候 采用加锁机制 ,使每次的执行结果和单线程执行的结果都是一样的,不存在执行程序时出现意外结果。
即A、B各买一张票,剩余98张票,数据正确。
线程不安全:指不考虑同步机制(synchronized等),有可能出现多个线程先后更改数据,造成所得到的数据是脏数据 (错误的)。
即如果是线程不安全,则可能会出现A、B各买一张票后,同时执行 100 - 1 操作 ,但剩余 99 张票,数据错误的情况。
即是否提供 加锁(同步)机制 ,是否存在数据 二义性 。线程不安全不代表数据一定会出问题,仅仅是可能出现脏数据问题,线程不安全 != 代码不安全
三、代码实例
1、不安全的买票
package xu.com.threads.chap1;
public class Tiket implements Runnable {
static int tiket = 10;
boolean flag = true;
@Override
public void run() {
while (flag) {
buy();
}
}
public void buy() {
//模拟买票
if (tiket <= 0) {
flag = false;
return;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) { //尝试解除阻塞异常
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "抢到了第" + tiket-- + "张票!");
}
public static void main(String[] args) {
Tiket tiket1 = new Tiket();
new Thread(tiket1, "黄牛").start();
new Thread(tiket1, "老师").start();
new Thread(tiket1, "我").start();
}
}
2、不安全的银行(两个人取钱)
package xu.com.threads.chap1;
public class UnsafeBank {
public static void main(String[] args) {
Account account = new Account(100, "结婚基金");
Draw boy = new Draw(account, 50, "boy");
Draw girl = new Draw(account, 100, "girl");
boy.start();
girl.start();
}
}
class Account {
int money;
String name;
public Account(int money, String name) {
this.money = money;
this.name = name;
}
}
class Draw extends Thread {
Account account;//账户
int drawingMoney; //取了多少钱
int nowMoney; //现在手里有多少钱
String name;
public Draw(Account account, int drawingMoney, String name) { //现在账户余额,取多少钱,手里有多少钱
this.account = account;
this.drawingMoney = drawingMoney;
this.name = name;
}
@Override
public void run() {
if (this.account.money - this.drawingMoney < 0) {
System.out.println(this.name + ",您的余额不足");
return;
}
try {
Thread.sleep(3000); //sleep放大问题的发生性,查缺补漏
} catch (InterruptedException e) {
e.printStackTrace();
}
this.account.money -= this.drawingMoney;//找到account堆内存地址,改变银行余额
this.nowMoney += this.drawingMoney;
System.out.println(this.name + "取到了" + this.nowMoney);
}
}
没有取钱之前银行的余额为100,两人去取钱,由于线程没有同步,导致,两人取的钱总共超出了银行的余额.。
3、 不安全的集合
package xu.com.threads.chap1;
import java.util.ArrayList;
public class UnsafeList {
public static void main(String[] args) throws InterruptedException {
ArrayList<String> list = new ArrayList<>(); //ArrayList集合线程不安全演示
for (int i = 1; i < 10000; i++) {
new Thread(
() -> { //lamda表达式
list.add(Thread.currentThread().getName());
}
).start();
}
Thread.sleep(1000);
System.out.println(list.size());
}
}
最后结果永远不会是10000个,因为线程不安全,有些数据会被别的线程覆盖掉
四、优化实例
线程同步有三种方式:
- 同步代码块
- 同步方法
- 锁机制
1、同步代码块:
synchronized
关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问
package xu.com.threads.chap1;
public class TicketSync implements Runnable {
// 总共一百张票
private int ticket = 10;
// 创建锁对象
Object lock = new Object();
/*
* 执行卖票操作
* */
@Override
public void run() {
// 窗口永远卖票
while (true) {
// 同步代码块
synchronized (lock) {
if (ticket > 0) {
// 这里把sleep去掉运行结果更明显
// 获取当前线程的名字
String name = Thread.currentThread().getName();
System.out.println(name + "正在卖: " + ticket--);
}
}
}
}
public static void main(String[] args) {
TicketSync ticket = new TicketSync();
Thread t1 = new Thread(ticket, "窗口一");
Thread t2 = new Thread(ticket, "窗口二");
Thread t3 = new Thread(ticket, "窗口三");
t1.start();
t2.start();
t3.start();
}
}
2、同步方法:
使用synchronized
修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着
// 同步方法格式
public synchronized void method(){
可能会产生线程安全问题的代码
}
package xu.com.threads.chap1;
/**
* @Description
* @auther 宁宁小可爱
* @create 2020-01-10 14:29
*/
public class Ticket implements Runnable {
// 总共一百张票
private int ticket = 10;
/*
* 执行卖票操作
* */
@Override
public void run() {
// 窗口永远卖票
while (true) {
sellTicket();
}
}
// 把存在线程安全问题的代码放入同步方法中
public synchronized void sellTicket() {
if (ticket > 0) {
// 获取当前线程的名字
String name = Thread.currentThread().getName();
System.out.println(name + "正在卖: " + ticket--);
}
}
public static void main(String[] args) {
Ticket ticket = new Ticket();
Thread t1 = new Thread(ticket, "窗口一");
Thread t2 = new Thread(ticket, "窗口二");
Thread t3 = new Thread(ticket, "窗口三");
t1.start();
t2.start();
t3.start();
}
}
3、Lock锁
java.util.concurrent.locks.Lock
机制提供了比synchronized
代码块和synchronized
方法更广泛的锁定操作, 同步代码块/同步方法具有的功能Lock
都有,除此之外更强大,更体现面向对象。
package xu.com.threads.chap1;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TicketLock implements Runnable {
// 总共一百张票
private int ticket = 10;
// 创建锁对象
Lock lock = new ReentrantLock();
/*
* 执行卖票操作
* */
@Override
public void run() {
// 窗口永远卖票
while (true) {
// 同步代码块 加锁
lock.lock();
if (ticket > 0) {
// 获取当前线程的名字
String name = Thread.currentThread().getName();
System.out.println(name + "正在卖: " + ticket--);
}
// 释放锁
lock.unlock();
}
}
public static void main(String[] args) {
TicketLock ticket = new TicketLock();
Thread t1 = new Thread(ticket, "窗口一");
Thread t2 = new Thread(ticket, "窗口二");
Thread t3 = new Thread(ticket, "窗口三");
t1.start();
t2.start();
t3.start();
}
}
五、ReentrantReadWriteLock
1、场景需求
- 对共享资源有读和写的操作,且写操作没有读操作那么频繁(读多写少)
- 在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源(读读可以并发)
- 但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写操作了(读写,写读,写写互斥)
- 在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。
针对这种场景,JAVA的并发包提供了读写锁ReentrantReadWriteLock,它内部,维护了一对相关的锁,一个用于只读操作,称为读锁;一个用于写入操作,称为写锁
读写锁机制:
读锁 | 写锁 | |
读锁 | 共享 | 互斥 |
写锁 | 互斥 | 互斥 |
2、 代码实例
验证读读共享模式
@Test
public void readReadMode() throws InterruptedException {
ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock r = rw.readLock();
ReentrantReadWriteLock.WriteLock w = rw.writeLock();
Thread thread0 = new Thread(() -> {
r.lock();
try {
Thread.sleep(1000);
System.out.println("Thread 1 running " + new Date());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
r.unlock();
}
},"t1");
Thread thread1 = new Thread(() -> {
r.lock();
try {
Thread.sleep(1000);
System.out.println("Thread 2 running " + new Date());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
r.unlock();
}
},"t2");
thread0.start();
thread1.start();
thread0.join();
thread1.join();
}
验证读写互斥模式
@Test
public void readWriteMode() throws InterruptedException {
ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock r = rw.readLock();
ReentrantReadWriteLock.WriteLock w = rw.writeLock();
Thread thread0 = new Thread(() -> {
r.lock();
try {
Thread.sleep(1000);
System.out.println("Thread 1 running " + new Date());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
r.unlock();
}
},"t1");
Thread thread1 = new Thread(() -> {
w.lock();
try {
Thread.sleep(1000);
System.out.println("Thread 2 running " + new Date());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
w.unlock();
}
},"t2");
thread0.start();
thread1.start();
thread0.join();
thread1.join();
}