java线程安全问题与死锁

线程通信
多线程的使用

一、问题的提出

在这里插入图片描述
例如:卖票的问题

package day9.ten;


/*
 *1.问题:卖票过程中,出现了冲票,错票-->出现了线程的安全问题
 * 2.问题出现的原因:当莫格线程操作车票的过程中,尚未操作完成时,其他线程参与进来
 * 3.如何解决:当一个线程在操作ticket的时候,其他线程不能参与进来,直到线程a操作完,其他线程才可以操作ticket。
 *    即使线程a出现了组设,也不能改变
 */

class Window1 implements Runnable{

    private int ticket = 100;

    @Override
    public void run() {
        while(true){
          
               if (ticket > 0) {
					//增加阻塞时间,阻塞时间越长,在这个范围内,出现的错误越多
                   try {
                       Thread.sleep(100);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }

                   System.out.println(Thread.currentThread().getName() + ":" + ticket);
                   ticket--;
               } else {
                   break;
               }
            
        }
    }
}


public class WindowTest1 {
    public static void main(String[] args) {
        Window1 w1 = new Window1();

        Thread t1 = new Thread(w1);
        Thread t2 = new Thread(w1);
        Thread t3 = new Thread(w1);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }

}

结果显示,有好多重复的票数,造成了多卖现象,多运行几次,票数还可能是负数
在这里插入图片描述

二、解决方法

方式一:同步代码块

实现Runnable接口

package day9.ten;


/*
 * 方式一:同步代码块
 * synchronized(同步监视器){
 *      //需要被同步的代码
 * }
 * 说明:1.操作共享数据的代码,即为需要被同步的代码, -->不能包含多了(可能只剩下一个线程做事),也不能包含代码少了,最好只包含共享数据
 *      2.共享数据:多个线程共同操作的变量。比如:ticket
 *      3.同步监视器,俗称:锁。任何一个类的对象,都可以充当锁
 *          要求:多个线程必须要公用统一把锁。
 *      补充:在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器
 */

class Window1 implements Runnable{

    private int ticket = 100;
    Object obj = new Object();
    @Override
    public void run() {
        while(true){
            synchronized (obj) {
                // obj可以是任何类的对象,包括你自己创的对象,当作锁,当一个线程操作共享资源时,就锁柱,只允许这个线程操作,只有这个线程操作好,其他线程才能操作
                if (ticket > 0) {

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName() + ":" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}


public class WindowTest1 {
    public static void main(String[] args) {
        Window1 w1 = new Window1();

        Thread t1 = new Thread(w1);
        Thread t2 = new Thread(w1);
        Thread t3 = new Thread(w1);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }

}

结果:线程安全
在这里插入图片描述
同步监视器:可以是任何对象,当你不想new对象是,也可以写入this,当前对象来充当同步监视器。

继承Thread实现线程安全

class Window extends Thread{
    private static int ticket = 100;

    private static Object obj = new Object();
    @Override
    public void run() {
        while(true){
//            synchronized (obj) {
            synchronized (obj){
                try {
                    sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (ticket > 0) {
                    System.out.println(getName() + ":票号:" + ticket);
                    ticket --;
                } else {
                    break;
                }
            }
        }

    }
}

public class WindowTest{

    public static void main(String[] args) {
        Window w1 = new Window();
        Window w2 = new Window();
        Window w3 = new Window();

        w1.setName("窗口1");
        w2.setName("窗口2");
        w3.setName("窗口3");

        w1.start();
        w2.start();
        w3.start();

    }
}

二者原理相同,不过这个需要把同步监视器需要的对象设置为静态的,要不然对象就不唯一了。
同样这个不需要创建对象时,可以使用Window.class,把当前类当作对象充当同步监视器

方式二:同步方法

实现Runnable接口的线程

package day9.ten;

/*
 *使用同步方法解决实现Runnable接口的线程安全问题
 * 把操作贡献资源的代码抽离出来,作为同步方法
 */
class Window3 implements Runnable{

    private int ticket = 100;

    @Override
    public  void run() {
        // 默认同步监视器:this
        while(true){
            show();
        }
    }
	// 把操作贡献资源的代码抽离出来,作为同步方法
    private synchronized void show(){
        if (ticket > 0) {

            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(Thread.currentThread().getName() + ":" + ticket);
            ticket--;
        }
    }
}


public class WindowTest3 {
    public static void main(String[] args) {
        Window1 w1 = new Window1();

        Thread t1 = new Thread(w1);
        Thread t2 = new Thread(w1);
        Thread t3 = new Thread(w1);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }

}

继承Thread类的线程

package day9.ten;

/*
 *使用同步方法解决继承Thread类的线程安全问题
 */

class Window4 extends Thread{
    private static int ticket = 100;


    @Override
    public void run() {
        while(true){
            show();
        }
    }
    public static synchronized void show(){
        // 同步监视器。需要设为静态方法
        try {
            sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (ticket > 0) {
            System.out.println(Thread.currentThread().getName() + ":票号:" + ticket);
            ticket --;
        }
    }
}

public class WindowTest4{

    public static void main(String[] args) {
        Window4 w1 = new Window4();
        Window4 w2 = new Window4();
        Window4 w3 = new Window4();

        w1.setName("窗口1");
        w2.setName("窗口2");
        w3.setName("窗口3");

        w1.start();
        w2.start();
        w3.start();

    }
}

关于同步方法的总结:

  • 1.同步方法仍然涉及到同步监视器,只是不需要我们显示的声明
  • 2.非静态的同步方法,同步监视器是:this
  • 静态的同步方法,同步监视器是:当前类本身

方式三:Lock锁 ---- JDK5.0新增

package day9.tenone;

/*
 *解决线程安全问题的方式三:Lock锁 ---- JDK5.0新增
 *
 * 1.面试题:synchronized与Lock的异同?
 *  相同:二者都可以解决线程安全问题
 *  不同:synchronized机制在执行完响应的同步代码以后,自动的释放同步监视器
 *       lock需要手动的启动同步(Lock()),同时结束同步需要手动的实现(unlock())
 */

import java.util.concurrent.locks.ReentrantLock;

class Window implements Runnable{

    public int ticket = 100;

    // 1.实例化ReentrantLock
    private ReentrantLock lock = new ReentrantLock(true);

    @Override
    public void run() {
         while(true){
             try {

                 // 2.调用lock()
                 lock.lock();
                 if (ticket > 0){
                     try {
                         Thread.sleep(100);
                     } catch (InterruptedException e) {
                         e.printStackTrace();
                     }

                     System.out.println(Thread.currentThread().getName() + ":售票,票号为" + ticket);
                     ticket --;
                 }else {
                     break;
                 }
             }finally {
                 //3.调用解锁方法
                 lock.unlock();
             }

         }
    }
}

public class LockTest {
    public static void main(String[] args) {
        Window w1 = new Window();
        Thread t1 = new Thread(w1);
        Thread t2 = new Thread(w1);
        Thread t3 = new Thread(w1);

        t1.setName("窗口一");
        t2.setName("窗口二");
        t3.setName("窗口三");

        t1.start();
        t2.start();
        t3.start();

    }

}

三、synchronized 与Lock的对比

  1. Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是
    隐式锁,出了作用域自动释放
  2. Lock只有代码块锁,synchronized有代码块锁和方法锁
  3. 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类

优先使用顺序:
Lock →同步代码块(已经进入了方法体,分配了相应资源)→同步方法(在方法体之外)

总结

实现Runnable接口的类,充当参数传进去Thread的构造器,不需要把变量,方法设为静态,就可以充当共享资源。而继承Thread类的线程,需要把变量,方法设置为静态的才能充当共享资源。
在线程安全中,主要是每次操作共享资源时,把这个操作锁住,防止其他线程操作,才能实现线程安全。锁是通过对象充当的,用对象当作锁去锁住操作,防止其他线程操作,当用锁锁住了这个操作,这个锁就在这里了,别人不能用。
同步的方式,解决了线程的安全问题。—好处
操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的的过程,效率低。

四、线程的死锁问题

  1. 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
  2. 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
  • 解决方法
    1. 专门的算法、原则
    2. 尽量减少同步资源的定义
    3. 尽量避免嵌套同步

死锁例子

/*
 *演示线程的死锁问题
 * 1.死锁的理解:不同的线程分别占用对方需要的同步资源不放弃,
 * 都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
 *
 * 2.说明:
 * 1)出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
 * 2)我们使用同步时,避免出现死锁
 *
 * 锁被一个线程使用之后,只能等待这个线程操作完,才能释放锁,期间别人拿不到。如果互相需要对面的锁,就形成了死锁,谁也得不到对方的,但是又需要对面的锁才能执行下一步
 */
public class ThreadTest {

    public static void main(String[] args) {
        StringBuffer s1 = new StringBuffer();
        StringBuffer s2 = new StringBuffer();

        new Thread(){
            @Override
            public void run() {

                synchronized (s1){
                    s1.append("a");
                    s2.append("1");

                    // 模拟阻塞状态
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    synchronized (s2){
                        s1.append("b");
                        s2.append("2");
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }.start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (s2) {
                    s1.append("c");
                    s2.append("3");

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (s1) {
                        s1.append("d");
                        s2.append("4");

                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }).start();
    }
}

四、小结释放锁的操作

  • 当前线程的同步方法、同步代码块执行结束。
  • 当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。
  • 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束。
  • 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当尊线程暂停,并释放锁。

五、不会释放锁的操作

  • 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行

  • 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)。

    应尽量避免使用suspend()和resume()来控制线程

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值