2021年一起学习Java多线程

1 基本概念

线程:进程可进一步细化为线程,是一个程序内部的一条执行路径。
①若一个进程同一时间并行执行多个线程,就是支持多线程的。
线程作为调度和执行的单位,每个线程拥有独立运行栈和程序计数器(pc),线程切换的开销小。
③一个进程(有一方法区、和堆)中的多个线程共享相同的内存单元/内存地址空间->它们从同意堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。

进程:是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程——生命周期 比如运行中的QQ

程序:是为了完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码。

单核CPU:其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程任务。因为CPU时间单元特别短,因此感觉不出来。

如果是多核的话,才能更好的发挥多线程的效率。

一个Java应用程序java.exe 其实至少有三个线程:main()主线程,gc()垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。

并发:多个CPU同时执行多个任务。比如:多个人同时做不同的事。

并行:一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事。

为什么使用多线程(优点)?
1.提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
2.提高计算机系统CPU的利用率
3.改善程序结构。将即长又复杂的进程分为多个线程,独立运行,利于理解和修改。

什么时候使用多线程?
1.程序需要同时执行两个或多个任务。
2.程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
3.需要一些后台运行的程序时。

2 线程的创建和使用

参考API文档

创建新执行线程有两种方法。

  • 继承Thread类
  • 实现Runnable接口
2.1创建的多线程的方式一(继承Thread类):
  1. 声明为Thread子类
  2. 重写Thread类中的run()
  3. 创建Thread子类的对象
  4. 该对象调用start()
//声明为Thread子类
class MyThread extends Thread {
    @Override
    //重写Thread类中的run()
    public void run() {
    	//遍历100以内的偶数
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

public class ThreadTest {
    public static void main(String[] args) {
        //创建Thread子类的对象
        MyThread m1 = new MyThread();
        //该对象调用start()
        m1.start();
        //m1.start(); //错误
    }
}

思考第一个问题:
如果我想要创建两个线程可不可以直接通过此对象调用两次start()呢?
在这里插入图片描述
当你调用两次start()就会报 -> IllegalThreadStateException

为什么会报这样一个错误呢?

证据一: 当启动线程时,如果threadStatus!=0就会抛出异常。换言之,线程的状态码等于0,正常调用start()方法。

 private volatile int threadStatus = 0;
 
 public synchronized void start() {
 	   //NEW状态 指的是创建一个新的线程
 	   /**
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();
}

证据二: 此方法的注释也有说明:如果当前线程已经启动,多次启动线程是不合法的

在这里插入图片描述
所以正确启动多个线程方式:

		MyThread m1 = new MyThread();
        MyThread m2 = new MyThread();
        m1.start();
        m2.start();

思考第二个问题:
此对象调用run()方法可以启动线程吗?

答曰:不可以

查看API文档可知start()方法的作用:
1.使该线程开始执行;
2.Java 虚拟机调用该线程的 run 方法。
在这里插入图片描述
验证:
通过获取当前线程名,就可以知道是否开启了新的线程

//稍微改造一下上面的代码
for (int i = 0; i < 100; i++) {
    if (i % 2 == 0) {
       System.out.println(Thread.currentThread().getName() + ":" + i);
    }
}

在这里插入图片描述
在这里插入图片描述
可以看到此对象调用start()方法,它的线程名是Thread-0,这就说明启动了线程
而线程名是main,这就说明了是主线程(主方法)调用了run()方法。

总结:

  1. 如果再启动一个线程,必须重新创建一个Thread子类的对象,不可以通过一个对象多次启动线程,会报错 IllegalThreadStateException。
  2. 线程的启动,必须调用start(),不能调用run()启动线程。

练习:创建两个线程 其中一个线程遍历100以内的偶数,另一个线程遍历100以内的奇数

public class PracticeThreadTest {
    public static void main(String[] args) {
        PraThread p1 = new PraThread();
        PraThread2 p2 = new PraThread2();
        p1.start();
        p2.start();
    }
}


class PraThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}
class PraThread2 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 != 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}
2.2Thread类中常用的方法:
  1. start() 启动当前线程 调用当前线程run()
  2. run() 需要重写Thread类中run方法,将创建的线程的操作声明在此方法中
  3. currentThread() 静态方法:返回当前正在执行的线程
  4. getName() 获取当前线程的名字
  5. setName() 设置当前线程的名字 ①构造器设置名字 ②此对象.setName()
  6. yield() 释放当前CPU执行权
  7. join() 线程a中调用线程b的join(),此时线程a进入阻塞状态,等待线程b全部执行完,线程a才结束阻塞状态
  8. stop() 方式已过时,当执行此方法是,强制结束当前线程
  9. sleep() 让当前线程睡眠指定的millis 毫秒,在指定的毫秒时间内,该线程是阻塞状态
  10. isAlive() 判断当前线程是否存活
  11. getPriority() 获取线程优先级
  12. setPriority() 设置线程优先级

例子1:
两种方式修改当前线程的名字:

  1. 初始化父类有参构造器的name属性
  2. 调用setName()方法
class ThreadMethod extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
    //方式一:
    public ThreadMethod(String name) {
        super(name);
    }
}


public class ThreadMethodTest {
    public static void main(String[] args) {
        ThreadMethod t1 = new ThreadMethod("子线程");
        //方式二:
        //t1.setName("子线程");
        t1.start();
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

例子2 测试yield():
测试结果: 正常情况来说两个线程是会交互的,多测试几次会明显的看到。

class ThreadMethod extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
            if (i % 20 == 0) {
                //当前线程释放CPU执行权
                yield();
            }
        }
    }
    public ThreadMethod() {
        super("子线程");
    }
}
public class ThreadMethodTest {
    public static void main(String[] args) {
        ThreadMethod t1 = new ThreadMethod();
        t1.start();
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

例子3 测试join():

class ThreadMethod extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
    public ThreadMethod(String name) {
        super(name);
    }
}

public class ThreadMethodTest {
    public static void main(String[] args) {
        ThreadMethod t1 = new ThreadMethod("子线程");
        t1.start();
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
            if (i == 20) {
                try {
                    t1.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

执行结果:
在这里插入图片描述

例子4 测试sleep():

class ThreadMethod extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                try {
                    sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
    }
    public ThreadMethod(String name) {
        super(name);
    }
}
public class ThreadMethodTest {
    public static void main(String[] args) {
        ThreadMethod t1 = new ThreadMethod("子线程");
        t1.start();
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

例子5 测试isAlive()

class ThreadMethod extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
    public ThreadMethod(String name) {
        super(name);
    }
}


public class ThreadMethodTest {
    public static void main(String[] args) {
        ThreadMethod t1 = new ThreadMethod("子线程");
        t1.start();
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
            if (i == 20) {
                try {
                    t1.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        boolean alive = t1.isAlive();
        System.out.println(alive);
    }
}

测试结果:
说明当前线程执行完声明的逻辑后,就死亡了。

false

2.3线程优先级

例子 测试:

  1. setPriority()方法
  2. getPriority()方法

Thread类中分别定义了最小、正常、最大优先级。
其中默认的优先级是NORM_PRIORITY = 5。

 /**
     * The minimum priority that a thread can have.
     */
    public final static int MIN_PRIORITY = 1;

   /**
     * The default priority that is assigned to a thread.
     */
    public final static int NORM_PRIORITY = 5;

    /**
     * The maximum priority that a thread can have.
     */
    public final static int MAX_PRIORITY = 10;
class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(getName()+ ":" + getPriority() +":" + i);
            }
        }
    }
    public MyThread(String name) {
        super(name);
    }
}
public class ThreadPriorityTest {
    public static void main(String[] args) {
        MyThread m1 = new MyThread("子线程");
        //设置子线程的优先级
        m1.setPriority(Thread.MAX_PRIORITY);
        //启动线程  调用run()
        m1.start();
        //设置主线程的名字
        Thread.currentThread().setName("主线程");
        //设置主线程的优先级
        Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + Thread.currentThread().getPriority() + ":" + i);
            }
        }
    }

}

在这里插入图片描述

说明:高优先级的线程不一定要执行完,才能执行低优先级的线程,只是高优先级的线程抢占CPU执行权的概率高于低优先级的线程

练习:模拟窗口买票,创建三个窗口买票 总票数100张 使用继承Thread类

代码如下:

class Window extends Thread{
	//三个窗口(线程)共享100张票
    private static int ticket = 100;
    @Override
    public void run() {
        while (true) {
            if (ticket > 0) {
            	//这里调用sleep()目的是提高重票和错票的概率
            	 try {
                    sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                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();
    }
}

出现线程安全问题(留到线程同步时候说明):
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.4创建的多线程的方式二(实现Runnable接口):
  1. 创建一个实现Runnable接口的类
  2. 实现类去实现Runnable中的抽象方法 run()
  3. 创建实现类的对象
  4. 该此对象作为参数传入Thread类的构造器中,创建Thread对象
  5. 通过Thread类的对象调用start()
//1.   创建一个实现Runnable接口的类
class MyThread2 implements Runnable{
	//2.   实现类去实现Runnable中的抽象方法 run()
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(i);
            }
        }
    }
}

public class ThreadTest2 {
    public static void main(String[] args) {
    	//3.   创建实现类的对象
        MyThread2 m1 = new MyThread2();
        //4.   该此对象作为参数传入Thread类的构造器中,创建Thread对象
        Thread t1 = new Thread(m1);
        //5.   通过Thread类的对象调用start() ①启动线程 ②调用当前线程run() -> 调用Runnable类型的target run()
        t1.start();
    }
}

为什么要将实现类的对象作为参数Thread类的构造器中,创建Thread对象?
源码:

 public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
 }

可以看见Thread类中,不止通过无参构造一种方式去创建Thread类对象,还可以通过传入一个target参数去创建Thread类对象,在这里我们直接传入Runnable接口的子类去完成创建。

谁调动了run方法?
继承Thread类,调用start()方法,不仅可以启动线程,而且也调用了run()。
实现Runnable接口的子类,调用start()方法,启动线程后,是target调用了run()方法
源码:

 @Override
    public void run() {
    	//已经创建了一个实现Runnable接口的类,所以target不等于null,满足条件。
        if (target != null) {
            target.run();
        }
    }

练习:模拟窗口买票,创建三个窗口买票 总票数100张 创建实现Runnable接口的类。

代码如下:

class Window implements Runnable{
    //这里区别于继承Thread类 不加static关键字  因为创建了一个对象本身就只有一份ticket属性
    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 WindowTest {
    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("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}

出现线程安全问题(留到线程同步时候说明):
在这里插入图片描述

两种创建多线程方式的比较
开发中:优先选择 实现Runnable接口的方式
原因:

  1. 实现的方式没有类的单继承性的局限性
  2. 实现的方式更适合来处理多个线程共享数据的情况

联系:Thread类实现了Runnable接口 -> public class Thread implements Runnable
相同点:两种方式都需要重写run(),将线程要执行的逻辑声明在run()中

3 线程的生命周期(状态)

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。
在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5 种状态。尤其是当线程启动以后,它不可能一直"霸占"着 CPU 独自运行,所以 CPU 需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换

JDK中用Thread.State类定义了线程的几种状态。

新建状态(NEW)
当程序使用 new 关键字创建了一个线程之后,该线程就处于新建状态,此时仅由 JVM 为其分配内存,并初始化其成员变量的值

就绪状态(RUNNABLE)
当线程对象调用了 start()方法之后,该线程处于就绪状态。Java 虚拟机会为其创建方法调用栈和程序计数器,等待调度运行。

运行状态(RUNNING)
如果处于就绪状态的线程获得了 CPU,开始执行 run()方法的线程执行体,则该线程处于运行状态。

阻塞状态(BLOCKED)

阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得 cpu timeslice 转到运(running)状态。阻塞的情况分三种:

一.等待阻塞(o.wait->等待对列):

  1. 运行(running)的线程执行 o.wait()方法,JVM 会把该线程放入等待队列(waitting queue)中。

二.同步阻塞(lock->锁池)

  1. 运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池(lock pool)中。

三.其他阻塞(sleep/join)

  1. 运行(running)的线程执行 Thread.sleep(long ms)或 t.join()方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者I/O
    处理完毕时,线程重新转入可运行(runnable)状态。

死亡状态(DEAD)
线程会以下面三种方式结束,结束后就是死亡状态。

正常结束

  1. run()或 call()方法执行完成,线程正常结束。

异常结束

  1. 线程抛出一个未捕获的 Exception 或 Error。

调用stop()

  1. 直接调用该线程的 stop()方法来结束该线程—该方法通常容易导致死锁,不推荐使用。

图解:
在这里插入图片描述

4 线程的同步

问题:多个线程操作同一个数据时出现重票、错票问题。
出现问题原因:当某个线程操作车票的过程中,尚未完成,另一个线程也参与进来,对车票进行操作。

解决方式:当一个线程a在操作ticket的时候,其他线程不能参与进来。 直到线程a操作完ticket时,其他线程才可以开始操作ticket。这种情况即使线程a出现了阻塞,也不能改变。

Java中通过同步机制,解决线程安全问题。

解决方式:

  1. 4.1同步代码块
//语法
synchronized(同步监视器) {
	//需要被同步的代码
}

什么是同步监视器?
答曰:同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
要求:多个线程必须要共用同一把锁。

什么是需要被同步的代码?
①操作共享数据的代码,即为需要被同步的代码
②共享数据:多个线程共同操作的变量,比如:ticket就是共享数据。

使用同步代码块解决窗口售票出现的多线程安全问题。
代码如下(继承Thread类):

class Window extends Thread {
    //多个线程共享一个变量
    private static int ticket = 100;
    //多个线程共用同一个属性
    private static Object obj = new Object();

    @Override
    public void run() {
        while (true) {
        	//同步监视器的两种方式:①类.class ②多个线程通用同一个对象属性。
            //synchronized (Window.class) {
            synchronized (obj) {
                if (ticket > 0) {
                    try {
                        sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    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();
    }
}

执行结果:
成功解决线程安全问题。
在这里插入图片描述

代码如下(实现Runnable接口的类):

class Window2 implements Runnable {
    private int ticket = 100;
    private Object obj = new Object();
    @Override
    public void run() {
        while (true) {
          //this指的是当前对象的引用  当前对象只有一个,引用也就有一个,符合同步监视器的条件
		  //synchronized (this) {
            synchronized (obj) {
                if (ticket > 0) {
                    try {
                        sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ": 卖票,票号为" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}

public class WindowTest2 {
    public static void main(String[] args) {
        Window2 w1 = new Window2();
        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();
    }
}
  1. 4.2 同步方法
//方法上加synchronized关键字
public synchronized int length() {
	return count;
}

同步方法: 也需要同步监视器,只是不需要我们显式声明
静态同步方法:同步监视器指的是当前类本身
非静态同步方法:同步监视器指的是 this

使用同步方法解决窗口售票出现的多线程安全问题。
代码如下(继承Thread类):

class Window3 extends Thread {
    //多个线程共享一个变量
    private static int ticket = 100;

    @Override
    public void run() {
        while (true) {
            play();
        }
    }

    public static synchronized void play() {
        if (ticket > 0) {
            try {
                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("窗口1");
        w2.setName("窗口2");
        w3.setName("窗口3");
        w1.start();
        w2.start();
        w3.start();
    }
}

代码如下(实现Runnable接口的类):

class Window4 implements Runnable {
    private int ticket = 100;

    @Override
    public void run() {
        while (true) {
           play();
        }
    }

    public synchronized void play() {
        if (ticket > 0) {
            try {
                sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ": 卖票,票号为" + ticket);
            ticket--;
        }
    }
}

public class WindowTest4 {
    public static void main(String[] args) {
        Window4 w1 = new Window4();
        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();
    }
}
  1. 4.3 Lock锁 — JDK5.0新增

1.从JDK5.0开始,Java提供了更强大的线程同步机制——通过显示定义同步锁对象来实现同步。同步锁使用Lock对象充当。

2.java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。

3.ReentrantLock 类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是 ReentrantLock ,可以显式加锁、释放锁。
4.通过有参构造创建ReentrantLock对象,其中参数fair表示公平。如果参数为true,则表示线程公平竞争,根据线程启动的顺序去执行。参考下面的代码的结果可知。

public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
}

使用Lock解决窗口售票出现的多线程安全问题。

class LockThread implements Runnable {
    private int ticket = 100;
    private ReentrantLock lock = new ReentrantLock(true);
    @Override
    public void run() {
        while (true) {
            try {
                //锁定方法
                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 {
                //解锁方法
                lock.unlock();
            }
        }
    }
}


public class LockTest {
    public static void main(String[] args) {
        LockThread lockThread = new LockThread();
        Thread t1 = new Thread(lockThread);
        Thread t2 = new Thread(lockThread);
        Thread t3 = new Thread(lockThread);
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        t3.start();
        t2.start();
        t1.start();
    }
}

执行结果:

在这里插入图片描述

综合练习:
银行有一个账户,有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。

分析:
是否存在多线程? 是
是否存在共享数据? 是 两个用户都操作账户
是否存在安全问题? 是

代码如下:

//账户
class Account {
    //账户余额
    private double balance;

    public Account(double balance) {
        this.balance = balance;
    }

    //存钱的方法
    public void deposit(double money) {
        synchronized (this) {
            if (money > 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                balance += money;
                System.out.println(Thread.currentThread().getName() + ": 存钱,余额为" + balance);
            }
        }
    }

}

//储户
class Consumer implements Runnable {
    //共享数据
    private Account account;

    public Consumer(Account account) {
        this.account = account;
    }

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            account.deposit(1000);
        }
    }
}

public class AccountTest {
    public static void main(String[] args) {
        Account acc = new Account(0);
        Consumer con = new Consumer(acc);
        Thread t1 = new Thread(con);
        Thread t2 = new Thread(con);
        t1.setName("甲");
        t2.setName("乙");
        t1.start();
        t2.start();
    }
}

执行结果:
在这里插入图片描述
面试题:synchronized 与 Lock的异同?

  • 相同:都是解决线程安全问题
  • 不同: synchronized机制在执行完相应的代码块以后,自动的释放同步监视器
  • Lock需要手动的启动同步锁(lock())、同时手动的释放锁(unLock())
4.4死锁问题:

死锁:
不同线程占用了对方需要的同步资源,都在等待对方放弃同步资源,就形成了线程的死锁。
简单来说:多个线程互相抱着对方需要的资源,然后形成僵持。
出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。

解决方法:
专门的算法、原则。
尽量减少同步资源的定义。
尽量避免嵌套同步

死锁的例子1

public class DeadLockTest {
    public static void main(String[] args) {

        StringBuffer s1 = new StringBuffer();
        StringBuffer s2 = new StringBuffer();
        new Thread(){
            @Override
            public void run() {
                //假设有线程a和线程b  线程一启动,如果
                //线程a拿到了s1锁,这时候睡了一秒 ,阻塞的过程中,很有可能线程b拿到了s2锁,
                //这时候线程b也睡了一秒,想要继续执行下去,必须拿到s1锁  发现s1锁已经被线程a拿到 无法执行下去
                //这时候就出现了 双方都占用这对方需要的共同资源,互相僵持这,出现 死锁。
                synchronized (s1) {
                    s1.append("a");
                    s2.append("1");
                    try {
                        sleep(1000);
                    } 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 {
                        sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (s1) {
                        s1.append("d");
                        s2.append("4");
                    }
                    System.out.println(s1);
                    System.out.println(s2);
                }
            }
        }).start();
    }
}

死锁的例子2:

//口红
class Lipstick {

}
//镜子
class Mirror {

}
class Makeup extends Thread {
    int choice;//选择
    String name; //化妆人的姓名
    //只有一份镜子和口红
    static Lipstick lipstick = new Lipstick();
    static Mirror mirror = new Mirror();

    public Makeup(int choice, String name) {
        this.choice = choice;
        this.name = name;
    }

    @Override
    public void run() {
        makeUp();
    }
    public void makeUp() {
        if (choice == 0) {
            synchronized (lipstick) {
                System.out.println(this.name + "拿到了口红");
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (mirror) {
                    System.out.println(this.name + "拿到了镜子");
                }
            }
        }else {
            synchronized (mirror) {
                System.out.println(this.name + "拿到了镜子");
                try {
                    sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lipstick) {
                    System.out.println(this.name + "拿到了口红");
                }
            }
        }
    }
}
public class DeadLockTest2 {
    public static void main(String[] args) {
        Makeup g1 = new Makeup(0,"灰姑娘");
        Makeup g2 = new Makeup(1,"白雪公主");
        g1.start();
        g2.start();
    }
}

解决方式:同步代码块不要嵌套。

面试题:synchronized 与 Lock的异同?

  1. 相同:都是解决线程安全问题
  2. 不同: synchronized机制在执行完相应的代码块以后,自动的释放同步监视器
    Lock需要手动的启动同步锁(lock())、同时手动的释放锁(unLock())
5 线程的通信

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

涉及到的三个方法:

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

说明:

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

代码如下:

class Number implements Runnable{
    private int number = 1;
    private Object obj = new Object();
    @Override
    public void run() {
        while (true) {
            /**
             * 解释 wait() notify()的过程
             * 当CPU将执行权分配给线程a时,线程a进入同步代码块(此时其他线程进不来)执行逻辑,
             * 当线程a调用wait()方法,线程a进入阻塞状态,并且释放同步监视器(也就是同步锁),这时CPU将执行权分配给了线程b
             * 线程b进入了同步代码块(锁好门),这时执行notify()唤醒线程a,线程a只能在同步代码块外面等着。
             *
             */
            //同步监视器可以是任何类的对象 此对象调用notify() 说明notify()被放在Object类中
            synchronized (obj) {
                obj.notify();
                if (number <= 100) {
                    try {
                        Thread.sleep(100);
                    } 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();
    }
}

执行结果:
在这里插入图片描述

面试题:sleep() 和 wait()的异同?

  1. 相同点:一旦执行放,都可以使得当前线程进入阻塞状态。
  2. 不同点:
    ①两个方法声明在不同类中:Thread类中声明静态的sleep(),Object类中声明wait()
    ②调用要求不同:sleep()可以在任何需要的场景下调用。wait()必须宰同步代码块或同步方法中调用
    ③关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。

生产者和消费者例题:
生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来去走产品。

分析:
1.是否是多线程问题? 是,生产者线程,消费者线程
2.是否有共享数据?是店员(或产品)
3.如何解决线程的安全问题?同步机制,有三种方法
4.是否涉及线程的通信?是

代码如下

//店员
class Clerk {
    //产品数量
    private int productCount;

    //生产产品的方法
    public synchronized void producerProduct() {
        if (productCount < 20) {
            productCount++;
            System.out.println(Thread.currentThread().getName() + ":开始生产第" + productCount + "个产品");
            notify();
        }else {
            //等待
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    消费产品的方法
    public synchronized void consumerProduct() {
        if (productCount > 0) {
            System.out.println(Thread.currentThread().getName() + ":开始消费第" + productCount + "个产品");
            productCount--;
            notify();
        }else {
            //等待
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
//消费者
class Consumer extends Thread {
    private Clerk clerk;

    public Consumer(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        System.out.println("消费者开始消费产品....");
        while (true) {
            try {
                sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.consumerProduct();
        }
    }
}

//生产者
class Producer extends Thread {
    private Clerk clerk;

    public Producer(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        System.out.println("生产者开始生产产品....");
        while (true) {
            try {
                sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.producerProduct();
        }
    }
}

public class ProducerTest {
    public static void main(String[] args) {
        Clerk clerk = new Clerk();
        Producer producer = new Producer(clerk);
        Consumer consumer = new Consumer(clerk);
        Consumer consumer2 = new Consumer(clerk);
        producer.setName("生产者1");
        consumer.setName("消费者1");
        consumer2.setName("消费者2");
        producer.start();
        consumer.start();
        consumer2.start();
    }
}
6 JDK5.0新增线程创建方式
  • 6.1实现C1allable接口
  1. 创建实现Callable接口的实现类
  2. 完成Callable接口的call()重写
  3. 创建接口的实现类对象
  4. 该对象作为参数传递给FutureTask类的构造器, 创建该类对象
  5. 此对象作为参数传递给Thread类的构造器,创建一个线程
  6. 调用futureTask的get()返回重写call()的返回值
//1.    创建实现Callable接口的实现类
class MyThread implements Callable<Integer>{
    //2.    完成Callable接口的call()重写
    @Override
    public Integer call() throws Exception {
        //完成100以内的偶数之和
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            if (i % 2 == 0) {
                System.out.println(i);
                sum+=i;
            }
        }
        return sum;
    }
}

public class ThreadTest {
    public static void main(String[] args) {
        //3.    创建接口的实现类对象
        MyThread m1 = new MyThread();
        //4.    该对象作为参数传递给FutureTask类的构造器, 创建该类对象
        FutureTask<Integer> futureTask = new FutureTask<Integer>(m1);
        //5.    此对象作为参数传递给Thread类的构造器,创建一个线程
        Thread t1 = new Thread(futureTask);
        t1.start();
        try {
            //6.    调用futureTask的get()返回重写call()的返回值
            Integer sum = futureTask.get();
            System.out.println("偶数总和: " + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

理解为什么通过FutureTask类的对象去创建线程。
在这里插入图片描述

  • 6.2使用线程池

背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。比如说:去一个地方你选择骑车去,应该避免自己去造车子,到达目的地后销毁这样的事情。应该去选择共享单车。
好处:
①提高响应速度(减少了创建新线程的时间)
②降低资源消耗(重复利用线程池中线程,不需要每次都创建)
③便于线程管理
corePoolSize:核心池的大小
maximumPoolSize:最大线程数
keepAliveTime:线程没有任务时最多保持多长时间后会终止

代码如下:

class NumberThread implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}
public class ThreadPool {
    public static void main(String[] args) {
        //1.    提供指定线程数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(10);
        //获得操作线程管理的类
        ThreadPoolExecutor poolExecutor = (ThreadPoolExecutor) service; 
        //设置核心池的大小
        poolExecutor.setCorePoolSize(20);
        //2.    执行指定的线程操作。需要提供实现Runnable接口或Callable接口实现类的对象
        service.execute(new NumberThread());//适用于Runnable
      //service.submit();//适用于Callable
        //关闭连接池
        service.shutdown();
    }
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值