在此之前,先让我们以一个简单的案例认识什么是线程安全问题
1.1线程安全问题
需求: 使用多线程模拟4个窗口共同卖100张电影票
public class MyRunnable implements Runnable {
// 共享变量--被4条线程共享
int tickets = 100;
@Override
public void run() {
// 线程的任务代码----->卖票
// 循环卖票,直到没有票为止
while (true) {
// 条件判断
if (tickets < 1){
break;
}
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":线程正在出售第" + tickets + "张票");
tickets--;
}
}
}
public class Test {
public static void main(String[] args) {
// 创建任务对象
MyRunnable mr = new MyRunnable();
// 创建并启动4条线程
new Thread(mr,"窗口1").start();
new Thread(mr,"窗口2").start();
new Thread(mr,"窗口3").start();
new Thread(mr,"窗口4").start();
/*
执行后出现的问题:
1.卖了重复票 eg:多个窗口共同卖了第100张票
2.卖了不存在的票 eg:多个窗口分别卖了第0,-1,-2张票
3.遗漏了票 eg: 第99,98,97张票没有出售
*/
}
}
-
原因: 线程的调度是抢占式
当某条线程在执行卖票的代码的时候,被其他线程干扰了,导致程序运行结果受影响 -
解决:
-当某条线程在执行卖票的代码的时候,不要被其他线程干扰了---->加锁
-synchronized—>同步代码块,同步方法
-Lock锁
1.2 synchronized
-
synchronized关键字:表示“同步”的。它可以对“多行代码”进行“同步”——将多行代码当成是一个完整的整体,一个线程如果进入到这个代码块中,会全部执行完毕,执行结束后,其它线程才会执行。这样可以保证这多行的代码作为完整的整体,被一个线程完整的执行完毕。
-
synchronized被称为“重量级的锁”方式,也是“悲观锁”——效率比较低。
-
synchronized有几种使用方式:
a).同步代码块b).同步方法【常用】
当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。
要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了同步机制(synchronized)来解决。
1.3 同步代码块
概述:使用synchronized关键字修饰的代码块就是同步代码块,表示只对这个区块的资源实行互斥访问
格式:
synchronized(锁对象){
// 代码块
}
锁对象:
语法的角度: 锁对象可以是任意类的对象
同步的角度: 多条线程想要实现同步,那么这多条线程使用的锁对象要一致(相同)
解决卖票案例问题:
public class MyRunnable implements Runnable {
// 共享变量--被4条线程共享
int tickets = 100;
@Override
public void run() {
// 线程的任务代码----->卖票
// 循环卖票,直到没有票为止
while (true) {
// 加锁
synchronized (this){
// 条件判断
if (tickets < 1) {
break;
}
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +
":线程正在出售第" + tickets + "张票");
tickets--;
}
// 释放锁
}
}
}
public class Test {
public static void main(String[] args) {
// 创建任务对象
MyRunnable mr = new MyRunnable();
// 创建并启动4条线程
new Thread(mr,"窗口1").start();
new Thread(mr,"窗口2").start();
new Thread(mr,"窗口3").start();
new Thread(mr,"窗口4").start();
}
}
1.4 同步方法
概述:使用synchronized关键字修饰方法就是同步方法,表示整个方法的资源实行互斥访问。
格式:
修饰符 synchronized 返回值类型 方法名(形参列表){
方法体
}
锁对象:
非静态同步方法锁对象是: this
静态同步方法锁对象是: 该方法所在类的字节码对象—>类名.class
解决卖票案例的问题:
public class MyRunnable implements Runnable {
// 共享变量--被4条线程共享
int tickets = 100;
@Override
public void run() {
// 线程的任务代码----->卖票
// 循环卖票,直到没有票为止
while (true) {
// 条件判断
if (sellTickets()) break;
}
}
// 非静态同步方法
private synchronized boolean sellTickets() {
if (tickets < 1){
return true;
}
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +
":线程正在出售第" + tickets + "张票");
tickets--;
return false;
}
}
public class Test {
public static void main(String[] args) {
// 创建任务对象
MyRunnable mr = new MyRunnable();
// 创建并启动4条线程
new Thread(mr,"窗口1").start();
new Thread(mr,"窗口2").start();
new Thread(mr,"窗口3").start();
new Thread(mr,"窗口4").start();
}
}
1.5 Lock锁
概述: 也是一种锁,他比synchronized更加强大,更加面向对象
使用:
-Lock是一个接口,Lock
实现提供了比使用 synchronized
方法和语句可获得的更广泛的锁定操作
-使用Lock就需要使用Lock接口的实现类ReentrantLock
-常用方法:
-void lock();加锁
-void unlock();释放锁
解决卖票案例问题:
public class MyRunnable implements Runnable {
// 共享变量--被4条线程共享
int tickets = 100;
// 创建Lock对象
Lock lock = new ReentrantLock();
@Override
public void run() {
// 线程的任务代码----->卖票
// 循环卖票,直到没有票为止
while (true) {
// 条件判断
// 加锁
lock.lock();
if (tickets < 1){
// 释放锁
lock.unlock();
break;
}
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +
":线程正在出售第" + tickets + "张票");
tickets--;
// 释放锁
lock.unlock();
}
}
}
public class Test {
public static void main(String[] args) {
// 创建任务对象
MyRunnable mr = new MyRunnable();
// 创建并启动4条线程
new Thread(mr,"窗口1").start();
new Thread(mr,"窗口2").start();
new Thread(mr,"窗口3").start();
new Thread(mr,"窗口4").start();
/*
注意:
1.线程锁对象没有释放,线程就不会销毁
2.子线程没有销毁,主线程就不能结束\销毁
*/
}
}