Java多线程学习笔记01:https://blog.csdn.net/weixin_44211980/article/details/106125754
3. 线程同步
3.1 案例引入
【卖票问题】
需求:某电影院目前正在上映国产大片,共有100张票,有3个窗口卖票,请设计一个程序模拟该电影院卖票
【实现思路】
- 定义一个SellTicket类实现Runnable类,定义一个成员变量tickets表示总票数
- 在SellTicket类中重写run()方法实现卖票
代码如下:
public class SellTicket implements Runnable {
private int tickets = 100;
private void sellticket() {
if(tickets > 0) {
System.out.println(Thread.currentThread().getName() + "正在售出第" + tickets + "张票");
--tickets;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
}
@Override
public void run() {
while(tickets > 0) {
sellticket();
}
}
}
public class ThreadStudy {
public static void selltickets() {
SellTicket st = new SellTicket();
Thread t1 = new Thread(st, "A");
Thread t2 = new Thread(st, "B");
Thread t3 = new Thread(st, "C");
//设置ABC三个窗口同时卖票
t1.start();
t2.start();
t3.start();
}
public static void main(String[] args) {
selltickets();
}
}
运行部分结果如下:
C正在售出第100张票
B正在售出第100张票
A正在售出第100张票
A正在售出第97张票
C正在售出第97张票
B正在售出第97张票
A正在售出第94张票
C正在售出第94张票
B正在售出第94张票
C正在售出第91张票
B正在售出第91张票
A正在售出第91张票
A正在售出第88张票
C正在售出第88张票
B正在售出第88张票
A正在售出第85张票
B正在售出第85张票
C正在售出第85张票
...
可以看到运行结果中同一张票被卖出了多次
【问题分析】
由于ABC三个进程是同时运行,那么当A进程运行到ticket–之前,B、C进程有可能会运行到输出一栏,此时你的ticket还没有改变,那么就会出现相同的票被卖出多次的情况,于是引入了线程数据安全问题
【卖票问题数据安全问题的解决】
多线程是否有数据安全问题的标准
- 是否是多线程环境
- 是否有共享数据
- 是否有多条语句操作共享数据
只要破坏了以上问题中的一个,那么数据安全问题就会被解决,由于此案例中多线程环境和共享数据必须被满足,那么我们尝试解决多条语句操作共享数据的问题
于是我们又引入了锁的概念,即将多条语句共享数据的代码块锁起来,让任意时刻只有一个线程执行即可
3.2 同步代码块
锁多条语句操作共享数据,可以使用同步代码块实现
格式:
synchronized(object) {
…
}
对于卖票问题,代码改进如下:
package StudyDemo02;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SellTicket implements Runnable {
private int tickets = 100;
private void sellticket() {
if(tickets > 0) {
System.out.println(Thread.currentThread().getName() + "正在售出第" + tickets + "张票");
--tickets;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
}
public void lock() {
synchronized(this) {
sellticket();
}
}
@Override
public void run() {
while(tickets > 0) {
lock();
sellticket();
}
}
}
运行结果如下:
A正在售出第100张票
A正在售出第99张票
A正在售出第98张票
C正在售出第97张票
C正在售出第96张票
C正在售出第95张票
C正在售出第94张票
C正在售出第93张票
C正在售出第92张票
B正在售出第91张票
B正在售出第90张票
B正在售出第89张票
B正在售出第88张票
B正在售出第87张票
...
3.3 线程安全的类
- StringBuffer
线程安全,可变字符序列
从JDK5开始被StringBuilder代替,通常应该使用StringBuilder类 - Vector
该类改进了List接口,使其成为Java.Collections.Framework的成员,如果不需要线程安全的实现,建议使用ArrayList代替Vector - Hashtable
该类实现了一个Hash表,他将键映射到值
如果不需要线程安全的实现,建议使用HashMap代替
3.4 Lock锁
虽然我我们呢可以理解代码同步块和同步方法的对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清楚的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock
Lock中常用函数
void lock(); 获得锁
void unlock(); 释放锁
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化
还是卖票问题,使用Lock的代码如下:
package StudyDemo02;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SellTicket implements Runnable {
private int tickets = 100;
private Lock lock = new ReentrantLock();
private void sellticket() {
if(tickets > 0) {
System.out.println(Thread.currentThread().getName() + "正在售出第" + tickets + "张票");
--tickets;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
} lock03() {
try {
lock.lock();
sellticket();
} finally { //为防止在锁的过程中出现问题导致无法解锁,在这里加上一个finally关键字表示无论如何也要解锁
lock.unlock();
}
}
@Override
public void run() {
while(tickets > 0) {
lock03();
}
}
}
Java多线程学习笔记03:https://blog.csdn.net/weixin_44211980/article/details/106146745