java多线程基础(二)

未来可期一位正行走在编程世界中的小白,希望能遇到更多正在努力中的小伙伴。

线程同步机制

一、背景

我以生活中的例子来打开这个问题,例如:我们做火车买票为例子。
创建个窗口,总票数为100张,使用实现Runable接口的方式

代码示例:

class Window1 implements Runnable{
    //总票数
    private int ticket = 100;
    @Override
    public void run() {
        while(true){
            if(ticket > 0){
                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("窗口一");
        t2.setName("窗口二");
        t3.setName("窗口三");

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

运行结果
java多线程基础(二)_第1张图片

  • 在卖票的过程中,通过上面的代码运行的代码,在最后出现了重票、错票,从而出现了线程的安全问题。
  • 出现线程安全的原因,就是当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,一起来操作车票。
  • 出现了问题,总要有解决的办法,在java多线程就给我们提供了这个解决办法方法。
  • 我先简单的描述一下解决问题的方法,当一个线程a在操作ticket的时候,其他线程不能参与进来。直到线程a操作完ticket时,其他线程才可以开始操作ticket.这种情况即使线程a出现了阻塞,也不能被改变。

二、解决方法

方式一:同步代码块

语法:
synchronized(同步监视器){
	// 需要不同的代码
}

说明:

  • 操作共享数据的代码,即为需要被同步的代码 --> 不能包含代码多了,也不能包含的代码少了。
  • 共享数据,多个数据共同操作的变量,比如:ticket就是共享数据。
  • 同步监视器俗称 。任何一个类的对象,都可以充当锁。
  • 要求:多个线程必须要用同一把锁。

① 解决继承Thread类线程安全问题

代码示例:

class Window2 extends Thread{
	//共享数据
    private static int ticket = 100;

    @Override
    public void run(){
        //添加同步代码块
        // Class clazz =Window2.class,其中Window2.class只会加载一次

            while(true){
                synchronized(Window2.class){
                    if(ticket > 0){
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(getName()+":卖票,票号为:"+ticket);
                        ticket--;
                    }else{
                        break;
                    }
                }
            }
    }
}
public class Window2Test {
    public static void main(String[] args) {
        Window2 t1 = new Window2();
        Window2 t2 = new Window2();
        Window2 t3 = new Window2();

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

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

运行结果:
java多线程基础(二)_第2张图片

② 解决实现Runable接口线程安全

代码示例:

class Window1 implements Runnable{
    //总票数
    private int ticket = 100;
    @Override
    public void run() {
        while(true){
            //设置同步代码块
            // 此时的this:唯一的Window1的对象
            synchronized(this){
                if(ticket > 0){
                    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("窗口一");
        t2.setName("窗口二");
        t3.setName("窗口三");

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

运行结果:
java多线程基础(二)_第3张图片
补充:

在实现Runable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。
在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类充当同步监视器。

方式二:同步方法

如果操作共享数据的代码完整的声明在一个方法中,我们不妨碍将此方法声明同步的

① 使用同步方法解决Thread类的线程安全问题

代码示例:

class Window3 extends Thread{
    private static  int ticket = 100;
    @Override
    public void run(){
      while(true){
          show();
      }
    }
    private static synchronized  void show(){ //同步监视器:Window3.class
        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) {
        Window3 w1 = new Window3();
        Window3 w2 = new Window3();
        Window3 w3 = new Window3();

        w1.setName("窗口一");
        w2.setName("窗口二");
        w3.setName("窗口三");

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

运行结果:
java多线程基础(二)_第4张图片

② 使用同步方法解决实现Runable接口线程安全

class Window4 implements Runnable{
    private int ticket = 100;
    @Override
    public void run() {
        while (true){
            show();
        }
    }
    private synchronized void show(){ //同步监视器:this
        if(ticket > 0){
            System.out.println(Thread.currentThread().getName()+":卖票,票号为:"+ticket);
            ticket--;
        }
    }
}
public class WindowTest4 {
    public static void main(String[] args) {
        Window4 w = new Window4();

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

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

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

java多线程基础(二)_第5张图片
关于同步方法的总结:

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

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

代码示例:

class Window5 implements Runnable{
    // 1.实例化ReentrantLock
    ReentrantLock lock = new ReentrantLock();
    private int ticket = 100;

    @Override
    public void run() {
        while(true){
            try {
                lock.lock();
                // 2. 调用锁定方法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.调用解锁方法:unlock()
                lock.unlock();
            }
        }
    }
}
public class LockTest {
    public static void main(String[] args) {
        Window5 w = new Window5();

        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

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

运行结果:
java多线程基础(二)_第6张图片

总结

使用的优先顺序

Lock --> 同步代码块(已经进入方法体,分配了相对应资源) --> 同步方法(在方法体之外)

利弊

  • 同步的方式,解决了线程的安全问题。–> 好处
  • 操作同步代码时,只能一个线程参与,其他线程等待。就相当于是一个的单线程的过程,效率低。

synchronzied 与 Lock 的异同

  • synchronzied机制在执行完相对应的同步代码块后,自动的释放同步监视器
  • Lock需要手动的启动(lock()),同时结束时也需要手动的实现(unlock());

死锁

死锁的理解:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁

说明:

  • 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
  • 我们使用同步时,要避免出现死锁。

代码示例:

class A {
    public synchronized void foo(B b) { //同步监视器:A类的对象:a
        System.out.println("当前线程名: " + Thread.currentThread().getName()
                + " 进入了A实例的foo方法"); // ①
//		try {
//			Thread.sleep(200);
//		} catch (InterruptedException ex) {
//			ex.printStackTrace();
//		}
        System.out.println("当前线程名: " + Thread.currentThread().getName()
                + " 企图调用B实例的last方法"); // ③
        b.last();
    }

    public synchronized void last() {//同步监视器:A类的对象:a
        System.out.println("进入了A类的last方法内部");
    }
}
class B {
    public synchronized void bar(A a) {//同步监视器:b
        System.out.println("当前线程名: " + Thread.currentThread().getName()
                + " 进入了B实例的bar方法"); // ②
//		try {
//			Thread.sleep(200);
//		} catch (InterruptedException ex) {
//			ex.printStackTrace();
//		}
        System.out.println("当前线程名: " + Thread.currentThread().getName()
                + " 企图调用A实例的last方法"); // ④
        a.last();
    }

    public synchronized void last() {//同步监视器:b
        System.out.println("进入了B类的last方法内部");
    }
}
public class DeadLock implements Runnable {
    A a = new A();
    B b = new B();

    public void init() {
        Thread.currentThread().setName("主线程");
        // 调用a对象的foo方法
        a.foo(b);
        System.out.println("进入了主线程之后");
    }

    @Override
    public void run() {
        Thread.currentThread().setName("副线程");
        // 调用b对象的bar方法
        b.bar(a);
        System.out.println("进入了副线程之后");
    }

    public static void main(String[] args) {
        DeadLock dl = new DeadLock();
        new Thread(dl).start();


        dl.init();
    }
}

运行结果:
java多线程基础(二)_第7张图片

线程通信

一、线程通信涉及到的三个方法

wait():一旦执行此方法,当前线程就会进入阻塞状态,并释放同步监视器。
notify():一旦执行方法,就会唤醒被wait的第一个线程,如果有多个线程被wait,就唤醒优先级最高的那个。
notifyAll():一旦执行此方法,就会唤醒所有被wait()的线程。

代码示例:

线程通信的例子:使用两个线程打印 1-100。线程1, 线程2 交替打印

class Number implements Runnable{
    private int number = 1;
    Object obj = new Object();
    @Override
    public void run() {
        while(true){
           synchronized(obj){
               //唤醒线程
               obj.notify();
               if(number <= 100){
                   try {
                       Thread.sleep(10);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
                   System.out.println(Thread.currentThread().getName()+":" +number);
                   number++;

                   try {
                       // 使得调用如下wait()方法的线程进入阻塞状态
                       obj.wait();
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
               }else{
                   break;
               }
           }
        }
    }
}
public class CommunicationTest {
    public static void main(String[] args) {
        Number number = new Number();
        Thread t1 = new Thread(number);
        Thread t2 = new Thread(number);

        t1.setName("线程1");
        t2.setName("线程2");

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

运行结果:
java多线程基础(二)_第8张图片

二、说明

wait(),notify().notifyAll():三个方法必须用在同步代码块或同步方法中。
wait(),notify(),notifyAll():三个方法的调用者必须是同步代码块同步方法的同步监视器。否则会出现IllegalMonitorStateException异常
wait(),notify(),notifyAll():三个方法时定义在java.lang.Object类中。

三、释放锁的操作:

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

四、不会释放锁的操作

  1. 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield(); 方法暂停当前线程的执行
  2. 线程执行同步代码块时,其他线程调用了线程的supend()方法将线程挂起,该线程不会释放锁(同步监视器)

五、小结

sleep()与wait()的异同

  1. 相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
  2. 不同点:
    <1>两个方法声明的位置不同:Thread类中声明sleep()Object类中声明wait()
    <2> 调用的要求不同:**sleep()**可以在任何需要的场景下调用。**wait()**必须使用在同步代码块或同步方法中。

java多线程基础到这里结束了,在写博客同时一边复习自己所学的知识点,给自己不断积累知识点。

你可能感兴趣的:(Java)