一、什么是线程安全问题
简单来说:多个线程同时操作同一个共享资源的时候可能会出现业务安全问题,称之为线程安全问题。
具体来说:由于操作系统中,线程的调度是抢占式执行的,或者说是随机的,这就造成线程调度执行时,线程的执行顺序是不确定的,虽然有一些代码在这种执行顺序不同的情况下也不会运行出错,但是还有一部分代码会因为执行顺序发生改变而受到影响,这就会造成程序出现Bug。对于多线程并发时会使程序出现bug的代码称作线程不安全的代码。
本质原因:线程在系统中的调度是无序的/随机的(抢占式执行)
二、线程不安全的原因
-
抢占式执行:罪魁祸首
-
多个线程同时修改同一个变量
-
修改操作不是原子的:多行指令,如果指令前后有依赖关系,不能插入其他影响自身线程执行结果的指令。
-
内存可见性:系统调用CPU执行线程内,一个线程对共享变量的修改,另一个线程能够立刻看到。
-
指令重排序:程序执行的顺序按照代码的先后顺序执行(处理器可能会对指令进行重排序)
三、解决线程安全性
3.1 synchronized
3.1.1 同步代码块
同步代码块的出现就是为了解决线程的安全问题,其主要作用就是把操作共享数据的代码锁起来。
同步代码块格式:
synchronized (锁){
操作共享数据的代码
}
- 特点1:锁默认打开,有一个线程进去了,锁自动关闭
- 特点2:里面的代码全部执行完毕,线程出来,锁自动打开
synchronized(任意对象):就相当于给代码加锁了,任意对象就可以看成是一把锁锁对象可以自己随意指定但是一定要是唯一的(建议:直接使用类的字节码文件)
3.1.2 同步方法
简单理解:就是把synchronized关键字加到方法上
同步方法格式:
修饰符 synchronized 返回值类型 方法名 (方法参数) {...}
同步方法的锁对象是什么呢?
this
静态同步方法格式:
修饰符 static synchronized 返回值类型 方法名(方法参数) {...}
同步静态方法的锁对象是什么呢?
类名.class
- 特点1:同步方法是锁住方法里面所有的代码
- 特点2:锁对象不能自己指定(this、字节码文件不用我们自己去写)
- 特点3:非静态锁用this,静态锁用当前类的字节码文件对象
3.1.3 案列
电影院新上映一部电影准备买票,共开设了3个窗口,票的总量为100张,请用多线程实现。
方式一:
package com.liming.mysynchronized;
public class MyTread extends Thread{
//表示这个类的所有对象可以共享ticket数据
static int ticket = 0;
@Override
public void run() {
while (true){
// 同步代码块
synchronized (MyTread.class){//锁对象,一定要是唯一的,同一个文件中字节码文件一定是唯一的
if (ticket < 100){
ticket++;
System.out.println(getName()+"正在卖第"+ticket+"张电影票");
}else {
break;
}
}
}
}
}
package com.liming.mysynchronized;
public class ThreadDemo {
public static void main(String[] args) {
MyTread t1 = new MyTread();
MyTread t2 = new MyTread();
MyTread t3 = new MyTread();
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
方式二:
package com.liming.mysynchronized;
public class MyRun implements Runnable{
int tick = 0;
@Override
public void run() {
while (true){
synchronized (this){
if (tick < 100){
tick++;
System.out.println(Thread.currentThread().getName()+"卖出了"+tick+"张电影票");
}else {
break;
}
}
}
}
}
package com.liming.mysynchronized;
public class MyThread01 {
public static void main(String[] args) {
MyRun mr = new MyRun();
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
Thread t3 = new Thread(mr);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
3.2 Lock锁
虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5之后提供了一个新的锁对象Lock锁。
3.2.1 Lock获取锁、释放锁
- void lock():获取锁
- void unlock():释放锁
上述两个方法都需要去手动开启
3.2.2 Lock的构造方法
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化ReentrantLock():创建一个Reentrantlock的实例。
代码演示:
package com.liming.mylock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyThread extends Thread{
static int ticket = 0;
static Lock lock = new ReentrantLock();
@Override
public void run() {
while (true){
lock.lock(); //上锁
try {
if (ticket == 100){
break;
}else {
Thread.sleep(10);
ticket++;
System.out.println(getName()+"卖了第"+ticket+"张票");
}
}catch (Exception e){
e.printStackTrace();
} finally {
lock.unlock();//释放锁
}
}
}
}
package com.liming.mylock;
public class ThreadDemo {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
转载自:Java 线程安全问题
网友评论