长达6500字的长文,彻底掌握Java多线程,这个你必须会。

Java多线程并发

进程与线程的联系和区别
进程

正在运行的程序,是系统资源分配和调用的独立单位。每一个进程都有它自己的内存空间和系统资源

线程

是进程的单个顺序控制流,或者说是一个单独执行的路径。一个进程如果只有一条执行路径,称之为单线程程序。一个进程如果有多条执行路径,称之为多线程程序。线程是包含在进程中的。

并行与并发

并行

并行是指在两个或多个事件在同一时刻发生。专业点来说就是:在同一时刻,有多条指定在多个处理器上同时执行。所以无论从宏观上还是微观上来看,二者都是一起执行的。

并发

并发是指两个多个事件在同一时间间隔发生。专业点来说就是:在同一时刻只能有一条指令执行,但是多个进行指令被快速轮换执行,使得在宏观上具有多个线程同时执行的效果,但在微观上并不是同时执行的。只是把时间分成若干段,使得多个进行快速交替执行。

Java此程序的运行原理

Java命令会启动JVM,等于启动了一个应用程序,也就是启动了一个进程。该进程会自动启动一个主线程,然后主线程去调用某个类的main方法。多以main方法是运行在主线程中的。也就是说呢,在之前的Java学习中写的程序都是单线程的。但是JVM本身启动时就是多线程,因为JVM至少有主线程和垃圾回收线程。

线程的调度

假设计算机只有一个CPU,CPU在某一个时刻只能执行一条指令,线程只有得到了CPU时间片,也就是使用权,才可以执行指令。那么具体应该是哪个线程得到CPU呢,这就回到了线程调度的问题。

  • 分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程所占用的CPU的时间。
  • 抢占式调度模型:优先级高的线程优先使用CPU。如果优先级相同,那么随机选择一个,优先级高的获得的CPU时间片相对多一些。java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃CPU。
多线程的实现方式
继承Thread类,并重写run()方法
package csu.edu.cn;

/**
 * 多线程实现方式一:继承Thread并重写run()方法
 */
public class Thread01 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(i);
        }
    }
}


package csu.edu.cn;

/**
 * 测试类
 */
public class Test01 {
    public static void main(String[] args) {
        //创建两个线程
        Thread01 thread01 = new Thread01();
        Thread01 thread02 = new Thread01();

        /**
         *两个线程都调用start方法
         * 然后由JVM去调用run()方法
         */
        thread01.start();
        thread02.start();

    }
}

结果:0-9打印两遍

特别注意:

  1. 不能直接用线程对象去调用run()方法,直接调用run()方法,就相当于调用普通方法,与多线程无关。要想看到多线程的效果就必须调用start()方法来启动。
  2. 调用run()与调用start()的区别:
    1. run()的调用仅仅是直接封装了被线程执行的代码,但是直接调用的话是普通方法的调用。
    2. start()方法的调用,首先单独启动了一个线程,然后再由JVM去该线程的run()方法
  3. 线程调用的是start()方法,而实际上却调用的是run()方法定义的主体。
  4. 同一个线程不能被start()两次。
获取和设置线程的名字
package csu.edu.cn;

/**
 * 给线程设置名字
 */
public class Thread02 extends Thread{
    public Thread02() {
        super();
    }
    public Thread02(String name){
        super(name);
    }


    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(getName()+"-----"+i);
        }
    }
}

package csu.edu.cn;

/**
 * 测试类
 */
public class Test02 {
    public static void main(String[] args) {
        //通过构造方法取名字
        Thread02 t1 = new Thread02("黄");
        Thread02 t2 = new Thread02("李");

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

        Thread02 t3 = new Thread02("脏");
        Thread02 t4 = new Thread02("刘");
        Thread02 t5 = new Thread02("怀");

        t3.start();
        t4.start();
        t5.start();


        //public static Thread currentThread()返回对当前正在执行的线程对象的引用
        //通过currentThread()获取当前的线程对象,再通过getName()获取线程名字
        //拿这个程序的main方法的主线程举例
        System.out.println(Thread.currentThread().getName());
    }
}


/**
李-----0
怀-----0
怀-----1
刘-----0
刘-----1
刘-----2
黄-----0
脏-----0
main
脏-----1
脏-----2
脏-----3
脏-----4
黄-----1
刘-----3
怀-----2
怀-----3
怀-----4
李-----1
李-----2
李-----3
李-----4
刘-----4
黄-----2
黄-----3
黄-----4
**/

结果分析:从此处的打印结果来看,线程之间是抢夺的方式来占据CPU的,这也我们上面说的Java虚拟机采用抢占式调度模型是一致的。只有占据了CPU的线程才能运行,第一次打印李-----0表示被t2抢夺到了CPU的执行权,但是只执行了一条语句CPU的时间就被夺走了。各个线程交错运行,每个线程只占据一小段时间,这样看起来像是一起运行的实际上不是。t1时间结束后就被t5抢占了CPU,打印了怀—0.

另外这里面还夹着一个字符串 “main”,这是System.out.println(Thread.currentThread().getName());语句的输出结果,因为main方法是被主线程调用的,也需要抢占CPU,所以打印时间也是不确定的。

实现Runable接口
package csu.edu.cn;

/**
 * 实现Runbale接口
 */
public class Thread03 implements Runnable{

    @Override
    public void run() {
            for(int i=0;i<10;i++){
                System.out.println(Thread.currentThread().getName()+"---"+i);
            }
    }
}
package csu.edu.cn;
/**
*测试类
*/
public class Test03 {
    public static void main(String[] args) {
        Thread03 thread03 = new Thread03();
        Thread t1 = new Thread(thread03);
        Thread t2 = new Thread(thread03);
        //事实上,当传入一个 Runnable target 参数给 Thread 后,Thread 的 run()方法就会调用target.run()
        t1.setName("A");
        t2.setName("B");

        t1.start();
        t2.start();
//        System.out.println(Thread.currentThread().getName());

    }
}


结果:
    B---0
    A---0
    B---1
    A---1
    A---2
    B---2
    A---3
    B---3
    B---4
    B---5
    B---6
    A---4
    B---7
    B---8
    A---5
    B---9
    A---6
    A---7
    A---8
    A---9
  1. 通过Runnable接口实现的类可以继承其他类,继承Thread的类不行。可以避免由于Java单继承带来的局限性。
  2. Runnable适合多个相同程序的代码去处理同一个资源的情况,比如
package csu.edu.cn;

/**
 * 使用Runable处理同一资源
 */
public class Thread04 implements Runnable{
    int num = 20;
    @Override
    public void run() {
        while(true){
            if(num>0){
                System.out.println(Thread.currentThread().getName()+"----"+(num--));
            }
        }
    }
}

package csu.edu.cn;

/**
 * 测试类
 */
public class Test04 {
    public static void main(String[] args) {
        Thread04 thread04 = new Thread04();

        Thread t1 = new Thread(thread04,"窗口一");
        Thread t2 = new Thread(thread04,"窗口二");
        Thread t3 = new Thread(thread04,"窗口三");


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

    }
}


结果:
    窗口一----20
    窗口三----18
    窗口二----19
    窗口三----16
    窗口一----17
    窗口三----14
    窗口二----15
    窗口三----12
    窗口一----13
    窗口三----10
    窗口二----11
    窗口三----8
    窗口三----6
    窗口三----5
    窗口一----9
    窗口一----3
    窗口一----2
    窗口一----1
    窗口三----4
    窗口二----7

可以看到这里是共享了同一个num

多线程-龟兔赛跑经典案例

package csu.edu.cn;

/**
 * 多线程----龟兔赛跑案例
 */
public class Race implements Runnable{
    private String winner;
    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {

            //如果是兔子,就休息
            if(Thread.currentThread().getName().equals("兔子") && i%10==0){
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            //判断比赛是否结束
            boolean flag =  gameOver(i);
            if(flag){
                break;
            }
            System.out.println(Thread.currentThread().getName()+"----->"+"跑了"+i+"步");
        }
    }


    private boolean gameOver(int steps){
        if(winner != null){
            return true;
        }else{
            if(steps>=100){
                winner = Thread.currentThread().getName();
                System.out.println("winner is "+winner);
                return true;
            }

        }
        return false;
    }


    public static void main(String[] args) {
        Race race = new Race();

        new Thread(race,"乌龟").start();
        new Thread(race,"兔子").start();
    }
}
实现Callable接口(了解即可)

核心方法:call()有返回值

package csu.edu.cn;

import java.util.concurrent.*;

public class Test05 implements Callable<Boolean>{

    String name;
    String age;

    public Test05(String name,String age) {
        this.age = age;
        this.name = name;
    }

    @Override
    public Boolean call() throws Exception {
        System.out.println(name+"---"+age);
        return true;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //第一步:创建服务
        Test05 t1 = new Test05("小鲤鱼","12");
        Test05 t2 = new Test05("灭聚石塔","23");
        Test05 t3 = new Test05("鳄鱼","18");

        //第二步:创建执行服务
        ExecutorService service = Executors.newFixedThreadPool(3);

        //第三步:提交执行
        Future<Boolean> r1 = service.submit(t1);
        Future<Boolean> r2 = service.submit(t2);
        Future<Boolean> r3 = service.submit(t3);

        //第四步:获取结果
        boolean rs1 = r1.get();
        boolean rs2 = r1.get();
        boolean rs3 = r1.get();

        //第五步:关闭服务
        service.shutdown();

    }
}

线程相关状态

  • 新建状态(NEW)

    • 即使用new新建一个线程,这个线程就处于新建状态
  • 运行状态(RUNABLE)

    • 操作系统中的就绪和运行两种状态,在Java中统称为RUNNABLE。
      • 就绪状态(READY):当线程对象调用了start()方法之后,线程就处于就绪状态,就绪意味着可以执行,但具体什么时候执行取决于JVM里线程调度情况。
      • 运行状态(RUNNING):当处于就绪状态的线程获得了CPU之后,真正开始执行run()方法的线程执行体时,就意味着该线程处于运行状态,但是对于但单处理器,一个时刻只能由一个线程处于运行状态。运行状态转变为就绪状态的情形:
        1. 线程失去处理器资源。线程不一定完整执行的,执行到一半,说不定就被别的线程抢走了。
        2. 调用yield()静态方法,暂时暂停当前线程,让系统的线程调度器重新调度一次,它自己完全有可能再次运行。
  • 阻塞状态(BLOCKED)

    • 阻塞状态表示线程正等待监视器锁,而陷入的状态。

      以下场景线程将会阻塞:

      • 线程等待进入synchronized同步方法。
      • 线程等待进入synchronized同步代码块。

      线程取得锁,就会从阻塞状态转变为就绪状态。

  • 等待状态(WAITING)

  • 超时等待状态(TIMED_WAITING)

  • 消亡状态

    • 线程的终止,表示线程已经执行完毕。前面已经说了,已经消亡的线程不能通过start再次唤醒。

      • run()和call()线程执行体中顺利执行完毕,线程正常终止
      • 线程抛出一个没有捕获的Exception或Error。

      需要注意的是:主线成和子线程互不影响,子线程并不会因为主线程结束就结束。

线程休眠sleep()方法

Thread.sleep()是Thread类的一个静态方法,使当前线程休眠,进入阻塞状态(暂停执行),如果线程在睡眠状态被中断,将会抛出IterruptedException中断异常。

线程休眠常用于做倒计时等作用

package csu.edu.cn;

/**
 * 线程休眠案例:倒计时
 */
public class TestSleep implements Runnable{
    @Override
    public void run() {
        for (int i = 10; i >= 0; i--) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("距离结束还剩:"+i+"秒");
        }
        System.out.println("时间结束!");
    }

    public static void main(String[] args) {
        TestSleep testSleep = new TestSleep();
        Thread thread = new Thread(testSleep);
        thread.start();
    }
}
//打印结果:
距离结束还剩:10秒
距离结束还剩:9秒
距离结束还剩:8秒
距离结束还剩:7秒
距离结束还剩:6秒
距离结束还剩:5秒
距离结束还剩:4秒
距离结束还剩:3秒
距离结束还剩:2秒
距离结束还剩:1秒
距离结束还剩:0秒
时间结束!

package csu.edu.cn;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.logging.SimpleFormatter;

/**
 * 线程休眠案例:钟表
 */
public class TestSleep implements Runnable{
    @Override
    public void run() {
        Date date = new Date(System.currentTimeMillis());
        while(true){
            System.out.println(new SimpleDateFormat("HH:mm:ss").format(date));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            date = new Date(System.currentTimeMillis());
        }
    }

    public static void main(String[] args) {
        TestSleep testSleep = new TestSleep();
        Thread thread = new Thread(testSleep);
        thread.start();
    }
}

打印结果:
    16:50:55
    16:50:56
    16:50:57
    16:50:58
    16:50:59
    16:51:00
    16:51:01
    16:51:02
    16:51:03
    16:51:04
    16:51:05
    16:51:06
    16:51:07
    16:51:08
    16:51:09
      ···

线程礼让yield()
package csu.edu.cn;

/**
 * 线程礼让
 */
public class TestYield implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"线程开始");
        Thread.yield();
        System.out.println(Thread.currentThread().getName()+"线程结束");
    }

    public static void main(String[] args) {
        TestYield testYield = new TestYield();
        Thread thread1 = new Thread(testYield);
        Thread thread2 = new Thread(testYield);

        thread1.start();
        thread2.start();

    }
}

打印结果:
Thread-1线程开始
Thread-0线程开始
Thread-0线程结束
Thread-1线程结束
类似于这样一人一句,进程占用CPU打印完后会礼让一下资源,当然礼让过后依然抢到了也是有可能的
线程强制执行join
package csu.edu.cn;

/**
 * 线程强制执行
 */
public class TestJoin implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("老子是VIP线程!!"+i);
        }
    }

    public static void main(String[] args) {
        TestJoin testJoin = new TestJoin();
        Thread thread = new Thread(testJoin);
        thread.start();
        //执行主线程
        for (int i = 0; i < 8; i++) {
            System.out.println("主线程---"+i);
            if(i==3){
                try {
                    thread.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

结果:
    主线程---0
    老子是VIP线程!!0
    主线程---1
    主线程---2
    主线程---3
    老子是VIP线程!!1
    老子是VIP线程!!2
    老子是VIP线程!!3
    老子是VIP线程!!4
    老子是VIP线程!!5
    老子是VIP线程!!6
    老子是VIP线程!!7
    老子是VIP线程!!8
    老子是VIP线程!!9
    主线程---4
    主线程---5
    主线程---6
    主线程---7
线程优先级
package csu.edu.cn;

public class TestPriority implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
        System.out.println(Thread.currentThread().getName()+"我正在运行"+"我的优先级是:"+Thread.currentThread().getPriority());

        }
    }


    public static void main(String[] args) {
        TestPriority testPriority = new TestPriority();
        Thread thread1 = new Thread(testPriority,"1");
        Thread thread2 = new Thread(testPriority,"2");
        Thread thread3 = new Thread(testPriority,"3");
        Thread thread4 = new Thread(testPriority,"4");
        Thread thread5 = new Thread(testPriority,"5");

        System.out.println(Thread.currentThread().getPriority());
        thread1.setPriority(2);
        thread2.setPriority(2);
        thread3.setPriority(3);
        thread4.setPriority(10);
        thread5.setPriority(3);

        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
        thread5.start();
    }
}


//结果:
5
1我正在运行我的优先级是:2
2我正在运行我的优先级是:2
3我正在运行我的优先级是:3
4我正在运行我的优先级是:10
4我正在运行我的优先级是:10
4我正在运行我的优先级是:10
3我正在运行我的优先级是:3
2我正在运行我的优先级是:2
5我正在运行我的优先级是:3
5我正在运行我的优先级是:3
1我正在运行我的优先级是:2
5我正在运行我的优先级是:3
2我正在运行我的优先级是:2
3我正在运行我的优先级是:3
1我正在运行我的优先级是:2

从上面的运行结果来看,默认的线程的优先级是5,最大是10,最小是1

线程的优先级最高并不意味着线程会优先执行,只是优先执行的概率会更高。

  1. 线程的默认优先级为5
  2. 线程优先级的范围是1-10
  3. 线程优先级高仅仅表示的是获取CPU时间片的几率会高一些,但不代表优先级高的线程一定能抢到CPU
守护线程

线程分为守护线程和用户线程,普通线程为用户线程,包括调用main方法的主线程也是用户线程,而垃圾回线程属于守护线程。

JVM必须等待用户线程执行完毕,但是不需要等待守护线程执行完毕 。守护线程一般是在用户线程结束后一段时间结束。下面用一个例子来说明这一点。

package csu.edu.cn;

/**
 * 守护线程实例
 */
public class TestDaemon implements Runnable{
    @Override
    public void run() {
         while(true){
             System.out.println("上帝一直在看着你的努力奋斗!!");
         }
    }

    public static void main(String[] args) {
        TestDaemon testDaemon = new TestDaemon();
        Thread thread = new Thread(testDaemon,"黄佳浩");
        thread.setDaemon(true);
        thread.start();
        for (int i = 0; i < 12; i++) {
            System.out.println("我是普通平民,正在寒窗苦读。。。。。");
        }
        System.out.println("我终于考上了中南大学!!");
    }
}

//运行结果:
我是普通平民,正在寒窗苦读。。。。。
我是普通平民,正在寒窗苦读。。。。。
我是普通平民,正在寒窗苦读。。。。。
我是普通平民,正在寒窗苦读。。。。。
我是普通平民,正在寒窗苦读。。。。。
我是普通平民,正在寒窗苦读。。。。。
我是普通平民,正在寒窗苦读。。。。。
我是普通平民,正在寒窗苦读。。。。。
我是普通平民,正在寒窗苦读。。。。。
我是普通平民,正在寒窗苦读。。。。。
我是普通平民,正在寒窗苦读。。。。。
我是普通平民,正在寒窗苦读。。。。。
我终于考上了中南大学!!
上帝一直在看着你的努力奋斗!!
上帝一直在看着你的努力奋斗!!
上帝一直在看着你的努力奋斗!!
上帝一直在看着你的努力奋斗!!
上帝一直在看着你的努力奋斗!!
上帝一直在看着你的努力奋斗!!
上帝一直在看着你的努力奋斗!!
上帝一直在看着你的努力奋斗!!
上帝一直在看着你的努力奋斗!!
上帝一直在看着你的努力奋斗!!
上帝一直在看着你的努力奋斗!!
上帝一直在看着你的努力奋斗!!
上帝一直在看着你的努力奋斗!!
上帝一直在看着你的努力奋斗!!

可以看到上面的与运行结果,表明上帝这个守护线程在用户线程执行结束后一段时间才停止运行。

线程同步机制
买火车票问题
package csu.edu.cn;

/**
 * 不安全的买票机制
 */
public class TestTicket {
    public static void main(String[] args) {
        BuyTicket buyTicket = new BuyTicket();
       new Thread(buyTicket,"张郑旭").start();
       new Thread(buyTicket,"史惠文").start();
        new Thread(buyTicket,"王睿").start();

    }

}


class BuyTicket implements Runnable{

    private int ticket = 10;
    @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--;  //语句三
            }
        }
    }
}

//打印结果:
王睿正在购买第 10 张票
张郑旭正在购买第 10 张票
史惠文正在购买第 10 张票
王睿正在购买第 7 张票
史惠文正在购买第 7 张票
张郑旭正在购买第 7 张票
史惠文正在购买第 4 张票
张郑旭正在购买第 4 张票
王睿正在购买第 4 张票
张郑旭正在购买第 1 张票
史惠文正在购买第 1 张票
王睿正在购买第 1 张票

结果分析

很明显我们看到了的现象是不同人正在购买同一张票,这就是线程不安全的表现之一。出现这个现象的重要原因就是CPU的操作具有原子性,单独执行一条指令后或者说语句,在执行完毕之前不会被中断。三个线程启动后,都会处于就绪状态,然后开始抢夺CPU执行语句。

  1. 语句①:Thread.sleep(1000);
  2. 语句②: System.out.println(Thread.currentThread().getName()+“正在购买第 “+ticket+” 张票”);
  3. 语句③: ticket–;

三条线程,假设线程1抢到了CPU,这时候就会开始执行语句,也就是至少会完成语句1,然后开始休眠。但是如果线程1不是休眠语句,那么线程1就可以继续往下执行,因为原子性,正在执行的语句不会被打断,所有只会在一条语句结束,下一条语句开始前,被抢走CPU或者中断,导致线程退出运行状态,转为就绪会阻塞状态。所以线程1可能一次性执行完了所有语句,也可能刚刚执行完就被抢走了CPU

接着线程2、3也抢到了CPU,也开执行语句1,然后也进入休眠状态,之后线程123从休眠中醒来,开始争夺CPU来完成语句2,但是都在执行语句3之前被抢走CPU执行语句2去了,所以一直没有进行减一操作。所以可能出现打印的都是正在去第十张票的现象。然后又开始抢夺CPU,接连执行三次-1的操作,然后又开始抢夺CPU执行语句1,所以出现以上打印的结果。

这是非常常见的线程不安全问题。

发生线程不安全的三个条件

  • 存在多个线程
  • 存在共享变量或i数据
  • 多条语句操作共享数据或变量
解决思路

可以看到火车票问题,条件1和条件2无法破坏,我们只能着手去破坏3。

思路是将多条语句包装程一个同步代码块,当某个线程执行到这个同步代码快的时候,就类似于原子性一样,其他线程无法抢占CPU,只能等待这个代码块执行完毕。

方法:

1.synchronized——自动锁

2.lock——手动锁

synchronized
synchronized(obj){
    //可能会发生线程安全问题的代码
}//这里的obj可以是任意对象,Object obj = new Object()

使用条件

  1. 必须有两个或两个以上的线程
  2. 同一时刻只有一个线程能够执行同步代码块
  3. 多个线程想要同步时,必须使用同一把锁。

使用过程

  1. 只有抢到锁的线程才能执行同步代码块,其余的线程即使抢到了CPU执行权,也只能被迫等待锁的释放。
  2. 代码执行完毕或者程序抛出异常都会释放锁,然后还未执行同步代码块的线程争夺锁,谁抢到谁执行同步代码块。

synchronized同步代码块改进买火车票问题

package csu.edu.cn;

/**
 * 不安全的买票机制
 */
public class TestTicket {
    public static void main(String[] args) {
        BuyTicket buyTicket = new BuyTicket();

       new Thread(buyTicket,"张郑旭").start();
       new Thread(buyTicket,"史惠文").start();
        new Thread(buyTicket,"王睿").start();



    }

}


class BuyTicket implements Runnable{

    private int ticket = 10;
    Object object = new Object();
    @Override
    public void run(){
        while(true){
            synchronized (object){
                if(ticket>0){
                    try {
                        Thread.sleep(5000);  //语句一
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName()+"正在购买第 "+ticket+" 张票");  //语句二
                    ticket--;  //语句三
                }
            }
        }
    }


}



//输出结果:
张郑旭正在购买第 10 张票
张郑旭正在购买第 9 张票
张郑旭正在购买第 8 张票
张郑旭正在购买第 7 张票
张郑旭正在购买第 6 张票
王睿正在购买第 5 张票
王睿正在购买第 4 张票
王睿正在购买第 3 张票
王睿正在购买第 2 张票
王睿正在购买第 1 张票

很明显我们可以看到,结果符合预期。

使用synchronized同步方法改进

package csu.edu.cn;

/**
 * 不安全的买票机制
 */
public class TestTicket_02 {
    public static void main(String[] args) {
        BuyTicket buyTicket = new BuyTicket();

        new Thread(buyTicket,"张郑旭").start();
        new Thread(buyTicket,"史惠文").start();
        new Thread(buyTicket,"王睿").start();



    }

}


class BuyTicket implements Runnable{

    private int ticket = 10;
    Object object = new Object();
    @Override
    public void run(){
        while(true){
            synchronized (object){
                if(ticket>0){
                    try {
                        Thread.sleep(5000);  //语句一
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    buyTicket();

                }
            }
        }
    }

    //同步方法,在权限修饰符后面加synchronized
    public synchronized void buyTicket(){
        if(ticket>0){
            System.out.println(Thread.currentThread().getName()+"正在购买第 "+ticket+" 张票");  //语句二
            ticket--;  //语句三
        }

    }


}

//打印结果
张郑旭正在购买第 10 张票
张郑旭正在购买第 9 张票
张郑旭正在购买第 8 张票
张郑旭正在购买第 7 张票
张郑旭正在购买第 6 张票
张郑旭正在购买第 5 张票
张郑旭正在购买第 4 张票
史惠文正在购买第 3 张票
王睿正在购买第 2 张票
王睿正在购买第 1 张票

使用同步方法的时候,注意是将synchronized加在权限修饰符后面。同时我们要知道这时候锁的对象时this。这种操作将上面的obj改为this的效果是一样的。

总结:一个线程使用同步方法,另一个线程使用this锁,可以实现同步。

静态同步方法(大致了解即可)

静态同步方法的锁对象是:这个静态同步方法所属的类的字节码文件

下面代码挺长的,但其实就修改了上面同步方法的代码的两处地方

public synchronized void buyTicket(){}改为
public synchronized static void sellTicket(){}
synchronized (this){}synchronized (BuyTicket.class){}
死锁问题
package csu.edu.cn;

public class DieLockTest extends Thread{
    public boolean flag;

    public DieLockTest(boolean flag){
        this.flag = flag;
    }
    @Override
    public void run() {
        if(flag){
            synchronized(LockObject.lockA){
                System.out.println("lockA");
                synchronized(LockObject.lockB){
                    System.out.println("lockB");
                }
            }
        }else{
            synchronized(LockObject.lockB){
                System.out.println("lockB");
                synchronized(LockObject.lockA){
                    System.out.println("lockA");
                }
            }
        }

    }
}

package csu.edu.cn;

public class LockObject {
    //两个锁对象
    public static final Object lockA = new Object();
    public static final Object lockB = new Object();
}


package csu.edu.cn;

public class TestDieLock {
    public static void main(String[] args) {
        DieLockTest t1 = new DieLockTest(true);
        DieLockTest t2 = new DieLockTest(false);
        t1.start();
        t2.start();
    }
}



//打印结果
lockA
lockB
//或者 
lockB
lockA

分析

可以看到,两个线程都在等待对方释放锁对象,然后进行下一步,但是两者都不释放锁。随即造成了死锁。

Lock

使用Lock解决线程不安全问题

package csu.edu.cn;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 不安全的买票机制
 */
public class TestTicket {
    public static void main(String[] args) {
        BuyTicket buyTicket = new BuyTicket();

       new Thread(buyTicket,"张郑旭").start();
       new Thread(buyTicket,"史惠文").start();
        new Thread(buyTicket,"王睿").start();



    }

}


class BuyTicket implements Runnable{

    private int ticket = 10;
    private Lock lock = new ReentrantLock();
    Object object = new Object();
    @Override
    public void run(){
        while(true){

               try{
                   lock.lock();
                   if(ticket>0){
                       try {
                           Thread.sleep(5000);  //语句一
                       } catch (InterruptedException e) {
                           e.printStackTrace();
                       }

                       System.out.println(Thread.currentThread().getName()+"正在购买第 "+ticket+" 张票");  //语句二
                       ticket--;  //语句三
                   }
               }finally {
                   lock.unlock();
               }

        }
    }


}

//打印结果
张郑旭正在购买第 10 张票
张郑旭正在购买第 9 张票
张郑旭正在购买第 8 张票
张郑旭正在购买第 7 张票
史惠文正在购买第 6 张票
史惠文正在购买第 5 张票
史惠文正在购买第 4 张票
王睿正在购买第 3 张票
王睿正在购买第 2 张票
王睿正在购买第 1 张票

//成功解决

写文章不易,给个赞吧!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值