Java中多线程安全、同步、死锁、等待唤醒机制

一、线程安全

如果有多个线程在同时运行,而这些线程可能会同时运行这段代码,程序每次运行结果和单线程运行的结果是一样的,而且程序中的变量值和和预期的一样,那么线程就是安全的,如果不是,则线程不安全。

下面通过售票的案例来理解一下线程安全问题

//模拟售票类
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();
}

运行结果如下:

窗口3100
窗口1100
窗口299
窗口398
窗口196
窗口297
窗口195
窗口293
窗口394
窗口192
窗口290
窗口391
窗口189
窗口287
窗口388
窗口186
窗口384
窗口285
窗口183
窗口381
窗口282
窗口180
窗口279
窗口378
窗口277
窗口376
窗口175
窗口174
窗口373
窗口272
窗口171
窗口270
窗口369
窗口168
窗口267
窗口366
窗口365
窗口163
窗口264
窗口362
窗口160
窗口261
窗口359
窗口257
窗口158
窗口156
窗口254
窗口355
窗口153
窗口351
窗口252
窗口150
窗口249
窗口348
窗口147
窗口245
窗口346
窗口144
窗口342
窗口243
窗口241
窗口139
窗口340
窗口138
窗口337
窗口236
窗口135
窗口334
窗口233
窗口332
窗口131
窗口230
窗口129
窗口328
窗口227
窗口126
窗口325
窗口224
窗口323
窗口122
窗口221
窗口120
窗口319
窗口218
窗口117
窗口316
窗口215
窗口114
窗口313
窗口212
窗口111
窗口310
窗口29
窗口18
窗口37
窗口26
窗口35
窗口14
窗口23
窗口32
窗口11

现象:不是每次执行都会出现线程安全问题,为了证明实验结果,多次执行,出现了上面的情况 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() 永远等待,直到被唤醒
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值