线程的同步
问题的提出
- 多个线程的不确定性引起执行结果的不稳定
- 多个线程对账本的共享,会造成操作的不完整性,会破坏数据。
- 例如:当你和媳妇同时取同一张卡的钱,就会造成数据安全问题。
线程安全问题,就是对数据的保护问题。???
解决卖票过程中,出现重票和错票问题 !
问题:
- 卖票过程中,出现重票和错票问题 ==> 出现了线程安全问题。
- 问题出现原因: 当某个线程操作车票的过程中, 尚未操作完成时,其他线程参与进来,也操作车票(也就是前一个线程进入时,运算还没又结束,并未修改票的数据,所以出现了,后面线程也可以通过判断条件进入代码块,执行购票过程。)
解决问题:
-
如何让一个线程操作时,其他线程不能进入操作。直到当前正在操作线程完成任务,其他才能进入。(就相当于上厕所,你在上厕所,把门锁了,其他人不能上,除非你上完厕所。)
-
引入: 同步机制
-
同步代码块
-
操作 共享数据(多个线程共同操作的变量)的代码,即为需要被同步的代码
-
通过 关键字 synchronized
-
synchronized (/*同步监视器, 锁*/) { // 同步代码块,需要同步的代码 }
-
同步监视器:俗称,锁。任何一个类的对象,都可以充当锁 (确保锁的唯一性)。
-
-
通过同步代码块,解决卖票的线程安全问题
-
package 实现Runnable; public class WindowStore { public static void main(String[] args){ Window window = new Window(); new Thread(window).start(); new Thread(window).start(); new Thread(window).start(); } } class Window implements Runnable{ private int tickets = 100; @Override public void run() { while (true) { synchronized (this) { if (tickets > 0) { try { Thread.sleep(100); // 模拟网络延时 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " 购买了第" + tickets-- + "票"); } else break; } } } }
-
-
-
同步方法
-
如果操作共享数据的代码完整的声明在一个方法中,可以将该方法声明为 同步方法
-
同步方法的声明
-
public synchronized void xxx() { // 共享数据操作过程。 }
-
-
同步方法,解决卖票问题
-
package 线程同步; public class WindowTicket { public static void main(String[] args){ Window window = new Window(); new Thread(window, "窗口1").start(); new Thread(window, "窗口2").start(); new Thread(window, "窗口3").start(); } } class Window implements Runnable{ private int tickets = 100; @Override public void run() { while (tickets > 0) { show(); } } public synchronized void show() { // 同步方法中,默认的同步监视器 就是 this if (tickets > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " 购买了第" + tickets-- + "票"); } } }
-
-
-
线程死锁问题
死锁
- 不同的线程分别占用对方需要的同步资源部放弃,都在等对方放弃自己需要的同步资源,就形成了线程的死锁。(也就像支付密码一人设置一般,谁也不高数谁,都在等对方说出口,者就是一个死锁问题)。
- 出现死锁问题后,不会出现异常,不会出现提示,只是所有线程都处于阻塞状态,无法继续
解决方法
- 专门的算法、原则
- 尽量减少同步资源的定义
- 尽量避免嵌套同步
Lock
- JDK5.0 开始, java提供了更强大的同步机制 —— 通过显示定义同步锁对象来实现同步。同步锁使用Lock对象充当。
- java.util.concurrent.locks.Lock 接口是控制多个线程对共享资源进行访问的工具。 锁提供了对共享资源的独占,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应该先获得Lock对象。
- ReentrantLock 类实现了 Lock, 他们拥有于 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentranLock, 可以显示的加锁,和解锁。
使用方式
-
先声明一个锁
private ReetrantLock lock = new ReentrantLcok();
-
用try 将同步代码块,包裹起来。
// 加锁 try { lock.lock(); // 加锁 if (ticket > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "票号为" + ticket --); } else break; }
-
在 finally 中解锁
finally { lock.unlock(); }
使用实例,卖票问题
import java.util.concurrent.locks.ReentrantLock;
public class LockTest {
public static void main(String[] args) {
Window window = new Window();
Thread t1 = new Thread(window);
Thread t2 = new Thread(window);
Thread t3 = new Thread(window);
t1.start();
t2.start();
t3.start();
}
}
class Window implements Runnable {
private int ticket = 100;
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while(true) {
try {
lock.lock(); // 加锁
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "票号为" + ticket --);
} else {
break;
}
} finally {
lock.unlock(); // 解锁
}
}
}
}
面试题目
-
synchronized 和 Lock 的异同?
同:二者都可以解决线程安全问题
异:synchronized 机制在执行完相应的同步代码以后,自动释放同步监视器
Lock需要手动启动同步(lock()), 同时结束同步也需要手动的实现 (unlock())
-
如何解决线程安全问题?有几种方式?
答:线程安全问题,解决的重点,是如何让单个共享资源,一次只能被一个线程处理。
解决方式有,两种:1. 同步代码块; 2. 同步方法。
练习题
-
两个人同时向同一账户存1000, 存三次,解决线程安全问题?
-
public class StoreMoney { public static void main(String[] args) { Account acct = new Account(0); Customer c1 = new Customer(acct); Customer c2 = new Customer(acct); c1.setName("甲"); c2.setName("乙"); c1.start(); c2.start(); } } class Account { private double balance; public Account(double balance) { this.balance = balance; } public synchronized void deposit(double amt) { if (amt > 0) { balance += amt; try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "存钱成功。余额为:" + balance); } } } class Customer extends Thread { private Account acct; public Customer(Account acct) { this.acct = acct; } @Override public void run() { for (int i = 0; i < 3; ++ i) { acct.deposit(1000); } } }