线程同步
卖票案例
-
案例需求
某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票
-
实现步骤
- 定义一个类SellTicket实现Runnable接口,里面定义一个成员变量:private int tickets = 100;
- 在SellTicket类中重写run()方法实现卖票,代码步骤如下
- 判断票数大于0,就卖票,并告知是哪个窗口卖的
- 卖了票之后,总票数要减1
- 票卖没了,线程停止
- 定义一个测试类SellTicketDemo,里面有main方法,代码步骤如下
- 创建SellTicket类的对象
- 创建三个Thread类的对象,把SellTicket对象作为构造方法的参数,并给出对应的窗口名称
- 启动线程
-
代码实现
public class Ticket implements Runnable { //票的数量 private int ticket = 100; @Override public void run() { while(true){ if(ticket <= 0){ //卖完了 break; }else{ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } ticket--; System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticket + "张票"); } } } } public class Demo { 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.setName("窗口一"); t2.setName("窗口二"); t3.setName("窗口三"); t1.start(); t2.start(); t3.start(); } }
运行结果出现了重复票,负号票问题:
线程安全问题-原因分析
1.卖票出现了问题
-
相同的票出现了多次
-
出现了负数的票
2.问题产生原因
线程执行的随机性导致的,可能在卖票过程中丢失cpu的执行权,导致出现问题
同步代码块解决数据安全问题
1.安全问题出现的条件
- 是多线程环境
- 有共享数据(哪些数据会被共享,哪些数据不会被共享?)
**会被共享的数据:**在Java中,所有实例域(对象的字段)、静态域(静态字段)和数组元素都存储在堆内存中,堆内存在线程之间共享
**哪些数据不会被共享:**局部变量(Local Variables),方法定义参数(局部变量(Local Variables)和异常处理器参数(ExceptionHandler Parameters)不会在线程之间共享)
- 有多条语句操作共享数据
2.卖票问题出现的原因(回顾)?如何解决呢?
线程的随机执行。如何解决呢?控制不同线程间操作发生相对顺序的机制线程同步
3.如何实现线程同步呢?
- 把多条语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可
- Java提供了同步代码块的方式来解决
同步代码块格式:
synchronized(任意对象) {
多条语句操作共享数据的代码
}
synchronized(任意对象):就相当于给代码加锁了,任意对象就可以看成是一把锁
4.synchronized同步代码块的特点?
默认情况下是打开的,只要有一个线程进去执行代码了,锁就会关闭
当线程执行完出来时,锁才会自动打开
代码演示
public class Ticket implements Runnable {
//票的数量
private int ticket = 100;
private Object obj = new Object();
@Override
public void run() {
while(true){
synchronized (obj){//多个线程必须使用同一把锁.
if(ticket <= 0){
//卖完了
break;
}else{
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticket + "张票");
}
}
}
}
}
public class Demo {
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.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
5.同步的好处和弊端
- 好处:解决了多线程的数据安全问题
- 弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率
同步方法解决数据安全问题
1.同步方法的格式
同步方法:就是把synchronized关键字加到方法上
修饰符 synchronized 返回值类型 方法名(方法参数) {
方法体;
}
2.同步方法的锁对象是什么呢?
this
3.静态同步方法的格式
同步静态方法:就是把synchronized关键字加到静态方法上
修饰符 static synchronized 返回值类型 方法名(方法参数) {
方法体;
}
4.同步静态方法的锁对象是什么呢?
类名.class
代码演示
public class MyRunnable implements Runnable {
private static int ticketCount = 100;
@Override
public void run() {
while(true){
if("窗口一".equals(Thread.currentThread().getName())){
//同步方法,锁对象为类名.class
boolean result = synchronizedMthod();
if(result){
break;
}
}
if("窗口二".equals(Thread.currentThread().getName())){
//同步代码块,这里设置为类名.class就是为了验证同步方法的锁对象为类名.class
synchronized (MyRunnable.class){
if(ticketCount == 0){
break;
}else{
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticketCount--;
System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticketCount + "张票");
}
}
}
}
}
private static synchronized boolean synchronizedMthod() {
if(ticketCount == 0){
return true;
}else{
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticketCount--;
System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticketCount + "张票");
return false;
}
}
}
public class Demo {
public static void main(String[] args) {
MyRunnable mr = new MyRunnable();
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
t1.setName("窗口一");
t2.setName("窗口二");
t1.start();
t2.start();
}
}
Lock锁
1.如何手动开关锁呢?
使用lock
2.如何使用Lock?
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化
ReentrantLock构造方法
方法名 | 说明 |
---|---|
ReentrantLock() | 创建一个ReentrantLock的实例 |
加锁解锁方法
方法名 | 说明 |
---|---|
void lock() | 获得锁 |
boolean tryLock() | 尝试获得锁 |
void unlock() | 释放锁 |
代码演示
import java.util.concurrent.locks.ReentrantLock;
public class Ticket implements Runnable {
//票的数量
private int ticket = 100;
private Object obj = new Object();
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
//synchronized (obj){//多个线程必须使用同一把锁.
try {
lock.lock();
if (ticket <= 0) {
//卖完了
break;
} else {
Thread.sleep(100);
ticket--;
System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticket + "张票");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
// }
}
}
}
public class Demo {
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.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
3.synchronized与Lock的区别
synchronized | Lock |
---|---|
Java的关键字,在jvm层面上 | 是一个类 |
1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁 | 在finally中必须释放锁,不然容易造成线程死锁 |
假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待 | 分情况而定,Lock有多个锁获取的方式,具体下面会说道,大致就是可以尝试获得锁,线程可以不用一直等待 |
锁的细粒度和灵活度低 | 锁的细粒度和灵活度高 |
4.如何选择?
除非synchronized不满足需要时,才选择Lock