一、线程安全
如果有多个线程在同时运行,而这些线程可能会同时运行这段代码,程序每次运行结果和单线程运行的结果是一样的,而且程序中的变量值和和预期的一样,那么线程就是安全的,如果不是,则线程不安全。
下面通过售票的案例来理解一下线程安全问题
//模拟售票类
public class Ticket implements Runnable{
int T = 100; //定义100张票
@Override
public void run() {
while (true)
{
try {
Thread.sleep(10); //加了休眠,让其他线程有机会执行
} catch (InterruptedException e) {
e.printStackTrace();
}
if(T > 0)
{
System.out.println(Thread.currentThread().getName() + T--);
}
}
}
}
public static void main(String[] args)
{
//创建Runnable接口类实现对象
Ticket t = new Ticket();
//创建三个Thread对象,传递Runnable类实现对象
Thread T1 = new Thread(t,"窗口1:");
Thread T2 = new Thread(t,"窗口2:");
Thread T3 = new Thread(t,"窗口3:");
//开启线程
T1.start();
T2.start();
T3.start();
}
运行结果如下:
窗口3:100
窗口1:100
窗口2:99
窗口3:98
窗口1:96
窗口2:97
窗口1:95
窗口2:93
窗口3:94
窗口1:92
窗口2:90
窗口3:91
窗口1:89
窗口2:87
窗口3:88
窗口1:86
窗口3:84
窗口2:85
窗口1:83
窗口3:81
窗口2:82
窗口1:80
窗口2:79
窗口3:78
窗口2:77
窗口3:76
窗口1:75
窗口1:74
窗口3:73
窗口2:72
窗口1:71
窗口2:70
窗口3:69
窗口1:68
窗口2:67
窗口3:66
窗口3:65
窗口1:63
窗口2:64
窗口3:62
窗口1:60
窗口2:61
窗口3:59
窗口2:57
窗口1:58
窗口1:56
窗口2:54
窗口3:55
窗口1:53
窗口3:51
窗口2:52
窗口1:50
窗口2:49
窗口3:48
窗口1:47
窗口2:45
窗口3:46
窗口1:44
窗口3:42
窗口2:43
窗口2:41
窗口1:39
窗口3:40
窗口1:38
窗口3:37
窗口2:36
窗口1:35
窗口3:34
窗口2:33
窗口3:32
窗口1:31
窗口2:30
窗口1:29
窗口3:28
窗口2:27
窗口1:26
窗口3:25
窗口2:24
窗口3:23
窗口1:22
窗口2:21
窗口1:20
窗口3:19
窗口2:18
窗口1:17
窗口3:16
窗口2:15
窗口1:14
窗口3:13
窗口2:12
窗口1:11
窗口3:10
窗口2:9
窗口1:8
窗口3:7
窗口2:6
窗口3:5
窗口1:4
窗口2:3
窗口3:2
窗口1:1
现象:不是每次执行都会出现线程安全问题,为了证明实验结果,多次执行,出现了上面的情况 100 出现了两次
分析:
- 当我们运行代码时,会发现出现了重复的票,这和我们预期的结果不一样,这就出现了多线程安全问题
- 多线程安全问题都是由全局变量及静态变量引起的,若每个线程中对全局变量、静态变量只有读操作没有写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,就可能有线程安全问题,一般采用线程同步来解决这个问题
- 在Java中抢占式调度,当程序在售票类运行的时候,进入run方法的while循环里的if循环的时候,可能没有抢到CPU而发生阻塞,并且这三个线程都有可能阻塞在这个位置,所以会出现打印出负数的情况(负数情况可自行测试)
二、线程同步
Java中提供了线程同步机制,有效的解决了线程安全问题,线程同步有两种方式:同步代码块,同步方法
1.同步代码块
格式:在代码块声明上,加上 synchronized
synchronized (锁对象) {
可能会产生线程安全问题的代码块
}
注:同步代码块中的锁对象可以是任意对象,但多个线程时,要使用同一个锁对象才能够保证线程安全
对上面的例子进行改进
public class Ticket implements Runnable{
int T = 10;
//定义锁对象
Object lock = new Object();
@Override
public void run() {
while (true)
{
//同步代码块
synchronized (lock)
{
if(T > 0)
{
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + T--);
}
}
}
}
}
对上述代码进行改进,增加了同步代码块,即同步锁,当线程进入同步代码块的时候,会判断有没有同步锁,如果有,则获取同步锁,进入同步中,去执行代码块,执行完毕后,从同步代码块出去,线程就将锁还回去;如果判断没有锁,就被阻挡在同步代码块外面不能执行,只能等待,这样,线程安全问题就解决了,但导致程序运行的速度下降了。
总结:没有锁的线程不能进入同步,在同步中的线程,不出去同步,就不会释放锁。
2.同步方法
(1)普通方法同步
格式:在方法声明上加上 synchronized
public synchronized void method() {
可能会产生线程安全问题的代码
}
注:同步方法中的锁对象是this
对上面案例进行改造:
public class Ticket implements Runnable{
int T = 10;
@Override
public void run() {
while (true)
{
//同步方法
method();
}
}
private synchronized void method()
{
if(T > 0)
{
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + T--);
}
}
}
同步方法也能解决线程安全问题
(2)静态同步方法
格式:在方法声明上加上 static synchronized
public static synchronized void method(){
可能会产生线程安全的代码
}
注:静态同步方法中的锁对象是 类名.class
三、死锁
在使用同步锁的时候,存在弊端:当线程任务中出现多个同步(多个锁)时,如果同步中嵌套了其他的同步。这时容易引发程序的无限等到,这种现象称为死锁。
如:
synchronized(A锁){
synchronized(B锁){
}
}
四、lock接口
Lock 接口实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作,Lock 接口中常用方法如下:
void lock():获得锁
void unlock():释放锁
使用 Lock 接口继续对售票案例进行修改:
public class Ticket implements Runnable{
int T = 10;
//创建Lock对象
Lock ck = new ReentrantLock();
@Override
public void run() {
while (true)
{
//调用lock方法获取锁
ck.lock();
if(T > 0)
{
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + T--);
}
//释放锁
ck.unlock();
}
}
}
五、等待和唤醒机制
等待唤醒机制是为了方便处理进程之间通信的手段,多个线程在处理同一个资源时,由于处理的动作(线程的任务)不同,为了使各个线程能够有效的利用资源,便采取了等待唤醒机制。等待唤醒机制涉及到的方法:
- wait():等待。将正在执行的线程释放其执行资格和执行权,并存储到线程池中
- notify():唤醒。唤醒线程池中被 wait() 的线程,一次唤醒一个,而且是任意的
- notifyAll():唤醒全部。可以将线程池中的所有 wati() 线程都唤醒
注:
- 这些方法都是在同步中才有效,在使用时必须注明所属锁,这样才可以明确出这些方法操作的到底是哪个锁上的线程
- 因为这些方法在使用的时候要注明所属的锁,而锁又是任意对象,所以这些方法是定义在 Object 类中的
举个栗子:
来看一个例子,现有Person类,存储了姓名和年龄,使用 InPut 线程对 Person 类输入信息,使用 OutPut 线程对 Person 类获取打印信息
//模拟Person类
public class Person {
String name;
int age;
boolean flag = false;
}
//输入线程任务InPut类
public class InPut implements Runnable {
private Person p;
int count = 0;
public inPut(Person p) {
this.p = p;
}
public void run() {
while (true)
{
synchronized (p)
{
if(p.flag)
{
try {
p.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if(count % 2 == 0)
{
p.name = "儿童";
p.age = 3;
}
else
{
p.name = "老人";
p.age = 99;
}
p.notify();
p.flag = true;
}
count++;
}
}
}
//输出线程任务OutPut类
public class OutPut implements Runnable {
private Person p;
public outPut(Person p)
{
this.p = p;
}
public void run() {
while (true)
{
synchronized (p)
{
if(!p.flag)
{
try {
p.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(p.name + ":" + p.age + "岁");
p.notify();
p.flag = false;
}
}
}
}
//在主线程中调用
public static void main(String[] args)
{
Person P = new Person();
InPut in = new InPut(P);
OutPut out = new OutPut(P);
Thread T1 = new Thread(in);
Thread T2 = new Thread(out);
T1.start();
T2.start();
}
分析:
- 输入 inPut 类:输入完成后,必须等待输出结果打印结束才能进行下一次赋值,赋值后,执行wait()方法永远等待,直到被唤醒,唤醒后重新对变量赋值,赋值后再唤醒输出线程 notify(),自己再wait()
- 输出 outPut 类:输出完成后,必须等待输入的重新赋值后才能进行下一次输出,在输出等待前,唤醒输入的notify(),自己再 wait() 永远等待,直到被唤醒