Java基础回顾-多线程

多线程

并发

指两个或多个时间再同一个时间段内发生(在多个任务中交替执行,一个一个的执行)

比喻:一个人吃两个馒头,一个一个吃

并行

指两个或多个时间再同一个时刻发生(同时发生)(多个任务同时执行)

比喻:两个人吃两个馒头,同时吃~


所以说,并行的速度更快(但是由于计算机处理事务特别快,所以当事务不是很多的时候,二者速度在我们人来感受起来,相差并不大)


线程与进程

进程:

指一个内存中正在运行的应用程序,即电脑当中每一个app都算一个进程~

每打开一个app(应用程序),就会进入内存产生一个进程~

线程:

先了解CPU(中央处理器):指挥电脑中的软件和硬件干活儿~

CPU的分类:

  1. AMD-----
  2. Inter-----Core(核心) i7 8866 四核八线程(8线程表示可以同时执行8个任务)
  3. 以电脑管家为例:每次使用电脑管家里的功能进行首页体检、病毒查杀、垃圾清理、电脑加速的时候,每一次都会开辟一条执行路径,这个路径就叫线程~

线程属于进程

是进程中的一个执行单元,负责程序的执行

假如CPU是单核心单线程CPU,它只能同时执行一个线程,如果要执行电脑管家,它就会在多个任务之间,高速的切换,轮流执行多个线程~

由于速度很快,就看起来像同时执行~(效率较低)

假如CPU是4核心8线程CPU,它就能同时执行8个线程,如果要执行电脑管家,它就会是8个线程在多个任务之间,高速的切换,那么它的速度就会是单核心单线程CPU的8倍(每个任务被执行到的几率都被提高8倍)!

由于速度很快,就看起来像同时执行~(效率较高)


多线程好处:

  1. 效率较高
  2. 多个线程之间互不影响

线程调度


分时调度

所有线程轮流使用CPU,每个线程使用CPU的时间平均分~(AA制


抢占式调度

把线程分优先级,优先级高的线程优先使用CPU(如果优先级一样,CPU就随机选一个线程)(VIP制

CPU都是在多个进程的多个线程之间进行高速切换,谁的优先级高,呢么被执行的机会就大一些~


主线程

指执行主方法(main方法)的线程~


单线程程序:Java程序中只有一个线程

程序从main方法开始,从上到下,按顺序执行

Person.java

public class Person {
    private String name;

    public void run(){
        //输出10次name
        for (int i = 1; i <= 10; i++) {
            System.out.println("第" + i +"次name:"+name+i);
        }
    }

    public Person() {
    }

    public Person(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                '}';
    }
}

PersonTest.java

public class PersonTest {
    public static void main(String[] args) {
        Person person1 = new Person("牛牛");
        Person person2 = new Person("niniu");
        person1.run();
        person2.run();
    }
}

运行结果:

这就是一个单线程程序~😐

假如在牛牛的run()方法niuniu的run()方法之间加一个异常(这里我用运算异常:除法分母不能为零的异常),会发现,niuniu就不打印了,如下图所示:

public class PersonTest {
    public static void main(String[] args) {
        Person person1 = new Person("牛牛");
        Person person2 = new Person("niniu");
        
        person1.run();
        
        //加入一个运算异常
        System.out.println(1/0);
        
        person2.run();
    }
}

运行结果:


由于单线程有弊端,所以我们学习创建多线程!😀

创建线程类(创建多线程)

Java使用java.lang.Thread类代表线程

Java虚拟机允许应用程序并发的运行多个执行线程-------->并发:同时发生

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

  • 一种是将类声明为Thread的子类------------------->该子类应该重写Thread类的run方法,接着该子类就可以分配并启动该子类的实例。

即创建一个自定义的子类去继承Thread类,然后重写里面的run();方法,然后在测试类中new刚刚创建的Thread的子类对象,并使用它去调用start();方法。如下代码和运行结果:

代码:

ChildreThread.java

public class ChildreThread extends Thread{
    private String name;

    //构造器
    public ChildreThread(String name) {
        this.name = name;
    }

    //重写run方法
    public void run(){
        //输出100次name
        for (int i = 1; i <= 100; i++) {
            System.out.println("第" + i +"次name:"+name+i);
        }
    }
}

ChildreThreadTest.java

public class ChildreThreadTest {
    public static void main(String[] args) {
        ChildreThread childreThread2 = new ChildreThread("start创建的多线程");
        childreThread2.start();//start创建的多线程


        //输出100次"主线程"
        for (int i = 1; i <= 100; i++) {
            System.out.println("第" + i +"次主线程");
        }
    }


}

运行结果:

发现两个线程在互相争抢运行~成功!😀

产生多线程的原理:

本来main函数可以自上而下执行一条main线程,但是中途出现了一个“childreThread2.start();//start创建的多线程”,于是main方法这条路径上就出现了一条新的线程路径(这条路径指向继承Thread的子类中的run();方法),于是乎,CPU就会对这两条线程路径进行随机选择,也可以说是这两条线程路径在争抢CPU的使用权,所以就出现了主线程和新创建的线程交替执行的画面~😂

多线程的内存图解:

优点:

每一个线程都在一个新的栈里,所以他们互不干扰~😀

  • 另一种是实现声明实现Runnable接口的类------>略

Thread类中的常用方法

  • public String getName();--------->获取当前线程名;
  • public void start();--------->导致该线程开始执行;Java虚拟机调用此线程的run方法;
  • public void run();--------->此线程要执行的任务在此处定义代码;
  • public static void sleep(long millis);--------->使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行);
  • public static Thread currentThread();--------->返回对当前正在执行的线程对象的引用;

获取线程名称

public String getName();--------->获取当前线程名:

MyThread.java

/*获取线程的名称:
*       1.使用Thread类中的getName()方法
*               String getName() 返回该线程的名称
*       2.可以先获取到当前正在执行的线程,使用线程中的方法getName()获取线程的名称
*               static Thread currentThread() 返回对当前正在执行的线程对象的引用。
* */

//第一步:定义一个Thread类的子类
public class MyThread extends Thread{
    //第二步:重写Thread类中的run方法,设置线程任务


    @Override
    public void run() {
        //获取线程的名称
        String name = getName();
        System.out.println(name);
    }
}

MyThreadTest.java

public class MyThreadTest {
    public static void main(String[] args) {
        //创建子类对象
        MyThread myThread = new MyThread();
        //调用start方法开启一个新线程
        myThread.start();//创建的新线程会去执行它自己重写的run方法,即MyThread类中的run方法
    }
}

运行结果:

如果再创建一个新线程:

public class MyThreadTest {
    public static void main(String[] args) {
        //创建子类对象
        MyThread myThread = new MyThread();
        //调用start方法开启一个新线程
        myThread.start();//创建的新线程会去执行它自己重写的run方法,即MyThread类中的run方法
        //如果再创建一个新线程
        new MyThread().start();//新线程的名称就会是Thread-1
        //如果又创建一个新线程
        new MyThread().start();//新线程的名称就会是Thread-2
        //以此类推.....还会有Thread-3,Thread-4,Thread-5,Thread-6...Thread-n
        new MyThread().start();
        new MyThread().start();
        new MyThread().start();
        new MyThread().start();
        new MyThread().start();
        new MyThread().start();

    }
}

运行结果:

在重写的run方法中,换另外一种方式获取线程名,即使用Thread类中的currentThread()方法,直接就可以获取到当前正在执行的线程,然后使用getName方法得到线程的名称!

代码如下:

MyThread.java

/*获取线程的名称:
*       1.使用Thread类中的getName()方法
*               String getName() 返回该线程的名称
*       2.可以先获取到当前正在执行的线程,使用线程中的方法getName()获取线程的名称
*               static Thread currentThread() 返回对当前正在执行的线程对象的引用。
* */

//第一步:定义一个Thread类的子类
public class MyThread extends Thread{
    //第二步:重写Thread类中的run方法,设置线程任务


    @Override
    public void run() {
        //获取线程的名称
//        String name = getName();
//        System.out.println(name);
        Thread thread = Thread.currentThread();//currentThread()或取当前线程
        System.out.println(thread+"------这是线程");//打印当前线程
        String threadName = thread.getName();//getName()获取线程名称
        System.out.println(threadName+"---------------------这是线程名称");//打印上一步获取到的线程名称
    }
}

MyThreadTest.java

public class MyThreadTest {
    public static void main(String[] args) {
        //创建子类对象
        MyThread myThread = new MyThread();
        //调用start方法开启一个新线程
        myThread.start();//创建的新线程会去执行它自己重写的run方法,即MyThread类中的run方法
        //如果再创建一个新线程
        new MyThread().start();//新线程的名称就会是Thread-1
        //如果又创建一个新线程
        new MyThread().start();//新线程的名称就会是Thread-2
        //以此类推.....还会有Thread-3,Thread-4,Thread-5,Thread-6...Thread-n
        new MyThread().start();
        new MyThread().start();
        new MyThread().start();
        new MyThread().start();
        new MyThread().start();
        new MyThread().start();

    }
}

运行结果:

代码优化:

使用一行代码(链式编程):

MyThread.java

/*获取线程的名称:
*       1.使用Thread类中的getName()方法
*               String getName() 返回该线程的名称
*       2.可以先获取到当前正在执行的线程,使用线程中的方法getName()获取线程的名称
*               static Thread currentThread() 返回对当前正在执行的线程对象的引用。
* */

//第一步:定义一个Thread类的子类
public class MyThread extends Thread{
    //第二步:重写Thread类中的run方法,设置线程任务


    @Override
    public void run() {
        //获取线程的名称
//        String name = getName();
//        System.out.println(name);
/*        Thread thread = Thread.currentThread();//currentThread()或取当前线程
        System.out.println(thread+"------这是线程");//打印当前线程
        String threadName = thread.getName();//getName()获取线程名称
        System.out.println(threadName+"---------------------这是线程名称");//打印上一步获取到的线程名称*/

        System.out.println(Thread.currentThread()+"------这是线程!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
        System.out.println(Thread.currentThread().getName()+"---------------------这是线程名称");
    }
}

MyThreadTest.java

public class MyThreadTest {
    public static void main(String[] args) {
        //创建子类对象
        MyThread myThread = new MyThread();
        //调用start方法开启一个新线程
        myThread.start();//创建的新线程会去执行它自己重写的run方法,即MyThread类中的run方法
        //如果再创建一个新线程
        new MyThread().start();//新线程的名称就会是Thread-1
        //如果又创建一个新线程
        new MyThread().start();//新线程的名称就会是Thread-2
        //以此类推.....还会有Thread-3,Thread-4,Thread-5,Thread-6...Thread-n
        new MyThread().start();
        new MyThread().start();
        new MyThread().start();
        new MyThread().start();
        new MyThread().start();
        new MyThread().start();

    }
}

运行结果:

获取主线程名称:

public class MyThreadTest {
    public static void main(String[] args) {
        //创建子类对象
        MyThread myThread = new MyThread();
        //调用start方法开启一个新线程
        myThread.start();//创建的新线程会去执行它自己重写的run方法,即MyThread类中的run方法
        //如果再创建一个新线程
        new MyThread().start();//新线程的名称就会是Thread-1
        //如果又创建一个新线程
        new MyThread().start();//新线程的名称就会是Thread-2
        //以此类推.....还会有Thread-3,Thread-4,Thread-5,Thread-6...Thread-n
        new MyThread().start();
        new MyThread().start();
        new MyThread().start();

        //获取主线程名称
        System.out.println(Thread.currentThread().getName());

    }
}

运行结果:


设置线程的名称

两种方式:

  1. 使用Thread类中的setName(自定义线程名)方法

  1. .创建一个带参数的构造方法,参数传递线程的名称;然后调用父类的带参数的构造方法,把线程名称传递个父类,让父类Thread给子线程起一个名字!

  • 使用Thread类中的setName(自定义线程名)方法

MyThread.java

/*设置线程的名称:
*       1.使用Thread类中的setName()方法
*                void setName(String name) 改变线程名称,使之与参数 name 相同。
*       2.创建一个带参数的构造方法,参数传递线程的名称;
*         然后调用父类的带参数的构造方法,把线程名称传递个父类,让父类Thread给子线程起一个名字!
* */

public class MyThread extends Thread{

    //方式2
    public MyThread() {
    }

    public MyThread(String name) {
        super(name);//将name传递给父类
    }

    @Override
    public void run() {
        //获取并打印线程名称
        System.out.println(Thread.currentThread().getName());
    }
}

MyThreadTest.java

public class MyThreadTest {
    public static void main(String[] args) {
        //new一个子类对象,开启新线程
        MyThread myThread = new MyThread();
        //先自定义一下线程名称,然后再开启新线程
        myThread.setName("niu_niu");
        myThread.start();

        //new一个子类对象并直接传入自定义线程名称参数,开启新线程
        MyThread myThread1 = new MyThread("传入自定义参数更改线程名称");
        myThread1.start();

        //获取主线程名称
        System.out.println(Thread.currentThread().getName());

    }
}

运行结果:


sleep类

public static void sleep(long millis);--------->使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行);

public class MyThreadTest {
    public static void main(String[] args) {
        //模拟秒表
        for (int i = 0; i <= 60; i++) {
            System.out.println(i);
            //使用Thread类的sleep方法让程序睡眠一秒钟
            try {
                Thread.sleep(1000);//静态方法通过类名直接调用,括号里单位为毫秒,1000毫秒==1秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

运行结果:(每过一秒打印一个数字)


创建多线程的第二种方式-Runable接口(推荐使用)

编写java.lang.Runable接口的实现类,并重写run()方法,该run()方法的方法体同样是该线程的线程执行体。

但是!java.lang.Runable接口里没有点start()方法,那么要怎么开启线程呢?

不急,这么来就行-------->先创建一个java.lang.Runable接口的实现类的实现类对象,然后把实现类对象当作参数传到Thread类里面,最后再去点start();这样就开启了一个新线程了~😁

public class MyThread implements Runnable{

    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        System.out.println("新线程名:"+name);
    }
}
public class MyThreadTest {

    public static void main(String[] args) {
        for (int i = 1; i <= 10; i++) {
            MyThread myThread = new MyThread();
            Thread thread = new Thread(myThread);
            thread.start();
    }


        //输出100次"主线程"
        for (int i = 1; i <= 10; i++) {
            System.out.println("第" + i +"次主线程");
        }
    }
}

运行结果:


实现Runable接口创建多线程的好处:

  • 避免了单继承的局限性----------------一个类只能继承一个类(一个人只能有一个亲爹!),类继承了Thread类就不能继承其他的类。用Runable接口创建多线程之后,子类还可以继承其他的类,实现其他的而接口!
  • 增强了程序的扩展性,降低了程序的耦合性(解耦)----------实现Runable接口的方式,把设置线程任务和开启新线程进行了分离(解耦)实现类中,重写的run方法就是用来设置线程要执行的任务;而创建Threrad类对象,调用start()方法,就是用来开启新线程;这样我们可以传给Thread类对象不同的参数,就可以控制线程执行不同的任务了~

匿名内部类的方式实现线程的创建

格式:

new 父类/接口(){

​ 重写父类/接口中的方法;

};

AnonymousInnerClass.java

public class AnonymousInnerClass {
    public static void main(String[] args) {
        //匿名内部类:线程的父类是Thread的方式
        new Thread(){
            @Override
            public void run() {
                System.out.println("新线程1:"+Thread.currentThread().getName());
            }
        }.start();
        //匿名内部类:线程的接口是Runable的方式
        Runnable runnable = new Runnable() {

            @Override
            public void run() {
                System.out.println("新线程2:" + Thread.currentThread().getName());
            }
        };
        new Thread(runnable).start();
    }
}

运行结果:


线程安全问题

/**
 * 实现多线程卖票案例
 */
public class MovieTicket implements Runnable{
    //首先定义一个共享票源
    private int ticket = 100;

    //设置线程任务:卖票
    @Override
    public void run() {
        //使用死循环让卖票操作一直重复
        while (true){
            //先判断电影票是否存在
            if (ticket>0){

                //我在这里为了使程序在实验的票数较少的情况下,出现重复卖票的几率增加,特意添加一个睡眠语句
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                //有票,(票递减)===>ticket--
                System.out.println(Thread.currentThread().getName()+"号窗口正在卖第"+ticket+"张票");
                ticket--;
            }
        }

    }
}
import junit.framework.TestCase;

public class MovieTicketTest extends TestCase {
    public static void main(String[] args) {
        //new一个Runable的实现类对象--->MovieTicket
        MovieTicket movieTicket = new MovieTicket();
        //用一个MovieTicket往三个线程的Thread里传,模拟电影院3个卖票窗口一起卖100张电影票的效果
        Thread thread1 = new Thread(movieTicket);
        Thread thread2 = new Thread(movieTicket);
        Thread thread3 = new Thread(movieTicket);
        //开启线程
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

运行结果:

出现了重复的票和不存在的票!!!!!!!!!!

即出现了线程安全的问题!!!!!!!!!!


出现线程安全问题的原理


解决线程安全问题

线程同步

线程同步方式1:同步代码块

synchronized关键字可以用于方法中的某个区块中,表示只对这个区块中的资源进行互斥访问。

格式:

synchroniuzed(同步锁){
    需要同步操作的代码(可能会出现线程安全问题的代码)(访问了共享数据的代码)
}

什么是同步锁?

对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁。

​ 1.锁对象可以是任意类型。

​ 2.必须保证多个线程对象要使用同一把锁。

​ 3.锁对象的作用:把同步代码块”锁住“,只让一个线程在代码块中执行

注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到同步锁,谁就进入代码块,此时其他的线程只能在外等候(BLOCKED)

/**
 * 实现多线程卖票案例
 */
public class MovieTicket implements Runnable{
    //首先定义一个共享票源
    private int ticket = 100;

    //创建一个锁对象(在run方法外面创建锁对象是因为,如果在run方法里面创建锁对象,那么每次开启一条线程就会创建一个锁随想,这样就不能保证锁对象唯一了)
    Object obj = new Object();

    //设置线程任务:卖票
    @Override
    public void run() {
        //使用死循环让卖票操作一直重复
        while (true){
            //创建同步代码块
            synchronized (obj){
                //先判断电影票是否存在
                if (ticket>0){

                    //我在这里为了使程序在实验的票数较少的情况下,出现重复卖票的几率增加,特意添加一个睡眠语句
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    //有票,(票递减)===>ticket--
                    System.out.println(Thread.currentThread().getName()+"号窗口正在卖第"+ticket+"张票");
                    ticket--;
                }
            }
        }
    }
}
import junit.framework.TestCase;

public class MovieTicketTest extends TestCase {
    public static void main(String[] args) {
        //new一个Runable的实现类对象--->MovieTicket
        MovieTicket movieTicket = new MovieTicket();
        //用一个MovieTicket往三个线程的Thread里传,模拟电影院3个卖票窗口一起卖100张电影票的效果
        Thread thread1 = new Thread(movieTicket);
        Thread thread2 = new Thread(movieTicket);
        Thread thread3 = new Thread(movieTicket);
        //开启线程
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

运行结果:没有出现重复票和不存在的票了,说明synchronized关键字有效解决了线程安全问题😀😀😀

该同步技术的原理

原理:使用了锁对象,这个锁对象又叫同步锁或对象锁,也叫对象监视器

个人理解:

当一个线程抢夺到CPU的执行权后,执行run方法时,会碰到synchronized代码块;

此时该线程就会检查synchronized代码块是否有锁对象,如果有,则会获取到锁对象,接着就执行同步代码块;

与此同时,另一个线程虽然也抢夺到了CPU的执行权,但是锁对象此时已将被占用,所以其他抢夺到CPU使用权的线程就无法获得锁对象,这就导致没有获得锁对象的线程无法进入同步代码块;

无法获得锁对象的线程就会进入阻塞状态,只能等待那个已经获取到锁对象的线程执行完毕后把锁对象释放出来

于是乎,当多线程执行时,大家都去争CPU并且还去争着获取唯一的锁对象,当某一个线程获取到锁对象的时候,其它线程就会进入阻塞状态,就只能干巴巴的等着锁对象被释放出来(获取到锁对象的线程执行完毕后就会把锁对象释放出来),然后大家再去抢夺被释放出来的、唯一的锁对象😶😶😶

这样就达到了同步代码块每次只能由某一个线程执行一次,这样模拟卖票程序就不会出现多个窗口卖同一张票或卖不存在的票的情况了~

线程同步方式2:同步方法

格式:

public synchroniuzed void method(){
    需要同步操作的代码(可能会出现线程安全问题的代码)(访问了共享数据的代码)
}

MovieTicket.java

/**
 * 实现多线程卖票案例
 */
public class MovieTicket implements Runnable{
    //首先定义一个共享票源
    private int ticket = 100;

    //创建一个锁对象(在run方法外面创建锁对象是因为,如果在run方法里面创建锁对象,那么每次开启一条线程就会创建一个锁随想,这样就不能保证锁对象唯一了)
    Object obj = new Object();

    //设置线程任务:卖票
    @Override
    public void run() {
        //使用死循环让卖票操作一直重复
        while (true){
            sellTickets();
        }
    }
    //定义一个同步方法sellTickets(卖票)
    public synchronized void sellTickets(){
        //先判断电影票是否存在
        if (ticket>0){

            //我在这里为了使程序在实验的票数较少的情况下,出现重复卖票的几率增加,特意添加一个睡眠语句
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            //有票,(票递减)===>ticket--
            System.out.println(Thread.currentThread().getName()+"号窗口正在卖第"+ticket+"张票");
            ticket--;
        }
    }
}

MovieTicketTest.java

import junit.framework.TestCase;

public class MovieTicketTest extends TestCase {
    public static void main(String[] args) {
        //new一个Runable的实现类对象--->MovieTicket
        MovieTicket movieTicket = new MovieTicket();
        //用一个MovieTicket往三个线程的Thread里传,模拟电影院3个卖票窗口一起卖100张电影票的效果
        Thread thread1 = new Thread(movieTicket);
        Thread thread2 = new Thread(movieTicket);
        Thread thread3 = new Thread(movieTicket);
        //开启线程
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

运行结果:

该同步技术的原理

原理:同步方法也会把方法内部的代码锁住,只让一个线程执行,它的锁对象就是线程的实现类对象

还有一种静态同步方法
/**
 * 实现多线程卖票案例
 */
public class MovieTicket implements Runnable{
    //首先定义一个共享票源
    private static int ticket = 100;//静态变量

    //创建一个锁对象(在run方法外面创建锁对象是因为,如果在run方法里面创建锁对象,那么每次开启一条线程就会创建一个锁随想,这样就不能保证锁对象唯一了)
    Object obj = new Object();

    //设置线程任务:卖票
    @Override
    public void run() {
        //使用死循环让卖票操作一直重复
        while (true){
            sellTicketsStatic();
        }
    }
    //定义一个同步方法sellTickets(卖票)
    public static synchronized void sellTicketsStatic(){//静态方法
        //先判断电影票是否存在
        if (ticket>0){

            //我在这里为了使程序在实验的票数较少的情况下,出现重复卖票的几率增加,特意添加一个睡眠语句
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            //有票,(票递减)===>ticket--
            System.out.println(Thread.currentThread().getName()+"号窗口正在卖第"+ticket+"张票");
            ticket--;
        }
    }
}
import junit.framework.TestCase;

public class MovieTicketTest extends TestCase {
    public static void main(String[] args) {
        //new一个Runable的实现类对象--->MovieTicket
        MovieTicket movieTicket = new MovieTicket();
        //用一个MovieTicket往三个线程的Thread里传,模拟电影院3个卖票窗口一起卖100张电影票的效果
        Thread thread1 = new Thread(movieTicket);
        Thread thread2 = new Thread(movieTicket);
        Thread thread3 = new Thread(movieTicket);
        //开启线程
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

运行结果:

静态同步方法的锁对象是实现类对象.class;(比如MovieTicket.classs)

线程同步方式3:Lock锁机制

Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的 Condition 对象。

Lock接口中的常用方法:

方法摘要
void lock() 获取锁。
void lockInterruptibly() 如果当前线程未被中断,则获取锁。
Condition newCondition() 返回绑定到此 Lock 实例的新 Condition 实例。
boolean tryLock() 仅在调用时锁为空闲状态才获取该锁。
boolean [tryLock](../../../../java/util/concurrent/locks/Lock.html#tryLock(long, java.util.concurrent.TimeUnit))(long time, TimeUnit unit) 如果锁在给定的等待时间内空闲,并且当前线程未被中断,则获取锁。
void unlock() 释放锁。

lock()获取锁unlock()释放锁使用步骤:3步

  1. 在成员位置创建一个ReentrantLock对象
  2. 在可能出现线程安全问题的代码前面调用lock()方法获取锁
  3. 在可能出现线程安全问题的代码后面调用unlock()方法释放锁

代码如下:

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

/**
 * 实现多线程卖票案例
 */
public class MovieTicket implements Runnable{
    //首先定义一个共享票源
    private int ticket = 100;

    //在成员位置创建一个`ReentrantLock`对象
    Lock lock = new ReentrantLock();


    //设置线程任务:卖票
    @Override
    public void run() {
        //使用死循环让卖票操作一直重复
        while (true){
            //在可能出现线程安全问题的代码前面调用`lock()`方法获取锁
            lock.lock();
            //先判断电影票是否存在
            if (ticket>0){

                //我在这里为了使程序在实验的票数较少的情况下,出现重复卖票的几率增加,特意添加一个睡眠语句
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                //有票,(票递减)===>ticket--
                System.out.println(Thread.currentThread().getName()+"号窗口正在卖第"+ticket+"张票");
                ticket--;
            }
            //在可能出现线程安全问题的代码后面调用`unlock()`方法释放锁
            lock.unlock();
        }
    }
}

结果略。

上述代码提高效率写法:

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

/**
 * 实现多线程卖票案例
 */
public class MovieTicket implements Runnable{
    //首先定义一个共享票源
    private int ticket = 100;

    //在成员位置创建一个`ReentrantLock`对象
    Lock lock = new ReentrantLock();


    //设置线程任务:卖票
    @Override
    public void run() {
        //使用死循环让卖票操作一直重复
        while (true){
            //在可能出现线程安全问题的代码前面调用`lock()`方法获取锁
            lock.lock();
            //先判断电影票是否存在
            if (ticket>0){

                //我在这里为了使程序在实验的票数较少的情况下,出现重复卖票的几率增加,特意添加一个睡眠语句
                try {
                    Thread.sleep(100);
                    //有票,(票递减)===>ticket--
                    System.out.println(Thread.currentThread().getName()+"号窗口正在卖第"+ticket+"张票");
                    ticket--;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    //在可能出现线程安全问题的代码后面调用`unlock()`方法释放锁
                    lock.unlock();//这样写,无论程序是否异常都会释放锁,提高程序效率
                }
            }
        }
    }
}

线程的状态

线程一共由6种状态:

  • NEW
    至今尚未启动的线程处于这种状态。
  • RUNNABLE
    正在 Java 虚拟机中执行的线程处于这种状态。
  • BLOCKED
    受阻塞并等待某个监视器锁的线程处于这种状态。
  • WAITING
    无限期地等待另一个线程来执行某一特定操作的线程处于这种状态。
  • TIMED_WAITING
    等待另一个线程来执行取决于指定等待时间的操作的线程处于这种状态。
  • TERMINATED
    已退出的线程处于这种状态。

图解:


等待唤醒案例:

void wait()
在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。

void notify()
唤醒在此对象监视器上等待的单个线程。

/*
    等待唤醒案例:线程之间的通信
        创建一个顾客线程(消费者):跟老板说要买的包子种类和数量,然后wait();等老板做包子--->进入WAITING(无限等待)
        创建一个老板线程(生产者):花了3秒做好了包子,然后notify();,唤醒顾客拿包子

    注意事项:
        顾客线程和老板线程,必须使用同步代码块包裹起来,保证等待和唤醒只能有一个在执行
        同步使用的锁对象必须保证唯一
        只有锁对象才能调用wait和notify方法

        void wait()
          在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。
        void notify()
          唤醒在此对象监视器上等待的单个线程。
          会继续执行wait方法之后的代码
 */
public class WaitAndNotify {
    public static void main(String[] args) {
        //创建锁对象,保证锁对象唯一
        Object obj = new Object();
        //创建一个顾客线程(消费者)
        new Thread(){
            @Override
            public void run() {
                while (true){//来个死循环,让顾客一直在这里等着买包子
                    //保证等待和唤醒的线程只能有一个执行,需要使用同步技术
                    synchronized (obj){
                        System.out.println("顾客:老板,给我来10个包子,要牛肉馅儿的.");
                        //顾客调用wait方法,开始进入无限等待状态(WAITING状态)
                        //由于父类的方法没有抛异常,所以我们也不能抛,就只能用try/catch
                        try {
                            obj.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        //这是加在wait方法之后的代码,是该线程被唤醒之后要接着执行的代码
                        System.out.println("顾客:嗯,不愧是陈记大包子,100块给你,哈哈~干饭干饭~真香啊!");
                      	System.out.println("========================================================");
                    }
                }
            }
        }.start();

        //创建一个老板的线程(生产者)
        new Thread(){
            @Override
            public void run() {
                while (true){//来个死循环,让老板一直在这里做包子
                    //花了10秒做好了包子
                    try {
                        Thread.sleep(1000*3);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //保证等待和唤醒的线程只能有一个执行,需要使用同步技术
                    synchronized (obj){
                        System.out.println("[3秒过了包子做好了,老板准备notify();告诉一直在等待的顾客...]");
                        //notify();,唤醒顾客拿包子
                        obj.notify();
                        System.out.println("老板:包子来嘞,总共10个包子,一共100块,拿好了您!祝您吃的开心值得愉快!");
                    }
                }
            }
        }.start();
    }
}

运行结果:


进入到TIMED_WAITING状态(计时等待)有两种方式: 1.使用sleep(long类型的毫秒值);方法,在sleep();结束后,该线程进入到RUNNABLE状态或BLOCK状态 2.使用wait(long类型的毫秒值);方法,wait方法结束后,如果没有被notify方法唤醒,就会自动醒来,醒来的线程进入到RUNNABLE状态或BLOCK状态

/*
    进入到TIMED_WAITING状态(计时等待)有两种方式:
        1.使用sleep(long类型的毫秒值);方法,在sleep();结束后,该线程进入到RUNNABLE状态或BLOCK状态
        2.使用wait(long类型的毫秒值);方法,wait方法结束后,如果没有被notify方法唤醒,就会自动醒来,醒来的线程进入到RUNNABLE状态或BLOCK状态

    两个唤醒的方法:
        1.void notify()
          唤醒在此对象监视器上(对象锁上)等待的单个线程
        2.void notifyAll()
          唤醒在此对象监视器上(对象锁上)等待的所有线程。
 */
public class WaitAndNotify {
    public static void main(String[] args) {
        //创建锁对象,保证锁对象唯一
        Object obj = new Object();
        //创建一个顾客线程(消费者)
        new Thread(){
            @Override
            public void run() {
                while (true){//来个死循环,让顾客一直在这里等着买包子
                    //保证等待和唤醒的线程只能有一个执行,需要使用同步技术
                    synchronized (obj){
                        System.out.println("顾客1:老板,给我来10个包子,要牛肉馅儿的.");
                        //顾客调用wait方法,开始进入无限等待状态(WAITING状态)
                        //由于父类的方法没有抛异常,所以我们也不能抛,就只能用try/catch
                        try {
                            obj.wait(3000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        //这是加在wait方法之后的代码,是该线程被唤醒之后要接着执行的代码
                        System.out.println("顾客1:嗯,不愧是陈记大包子,100块给你,哈哈~干饭干饭~真香啊!");
                        System.out.println("========================================================");
                    }
                }
            }
        }.start();
    }
}

运行结果:


两个唤醒的方法: 1.void notify() 唤醒在此对象监视器上(对象锁上)等待的单个线程 2.void notifyAll() 唤醒在此对象监视器上(对象锁上)等待的所有线程

1.void notify()

/*
    进入到TIMED_WAITING状态(计时等待)有两种方式:
        1.使用sleep(long类型的毫秒值);方法,在sleep();结束后,该线程进入到RUNNABLE状态或BLOCK状态
        2.使用wait(long类型的毫秒值);方法,wait方法结束后,如果没有被notify方法唤醒,就会自动醒来,醒来的线程进入到RUNNABLE状态或BLOCK状态

    两个唤醒的方法:
        1.void notify()
          唤醒在此对象监视器上(对象锁上)等待的单个线程
        2.void notifyAll()
          唤醒在此对象监视器上(对象锁上)等待的所有线程。
 */
public class WaitAndNotify {
    public static void main(String[] args) {
        //创建锁对象,保证锁对象唯一
        Object obj = new Object();
        //创建一个顾客线程(消费者)
        new Thread(){
            @Override
            public void run() {
                while (true){//来个死循环,让顾客一直在这里等着买包子
                    //保证等待和唤醒的线程只能有一个执行,需要使用同步技术
                    synchronized (obj){
                        System.out.println("顾客1:老板,给我来10个包子,要牛肉馅儿的.");
                        //顾客调用wait方法,开始进入无限等待状态(WAITING状态)
                        //由于父类的方法没有抛异常,所以我们也不能抛,就只能用try/catch
                        try {
                            obj.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        //这是加在wait方法之后的代码,是该线程被唤醒之后要接着执行的代码
                        System.out.println("顾客1:嗯,不愧是陈记大包子,100块给你,哈哈~干饭干饭~真香啊!");
                        System.out.println("========================================================");
                    }
                }
            }
        }.start();

        //创建一个顾客线程(消费者)
        new Thread(){
            @Override
            public void run() {
                while (true){//来个死循环,让顾客一直在这里等着买包子
                    //保证等待和唤醒的线程只能有一个执行,需要使用同步技术
                    synchronized (obj){
                        System.out.println("顾客2:老板,给我来10个包子,要牛肉馅儿的.");
                        //顾客调用wait方法,开始进入无限等待状态(WAITING状态)
                        //由于父类的方法没有抛异常,所以我们也不能抛,就只能用try/catch
                        try {
                            obj.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        //这是加在wait方法之后的代码,是该线程被唤醒之后要接着执行的代码
                        System.out.println("顾客2:嗯,不愧是陈记大包子,100块给你,哈哈~干饭干饭~真香啊!");
                        System.out.println("========================================================");
                    }
                }
            }
        }.start();

        //创建一个老板的线程(生产者)
        new Thread(){
            @Override
            public void run() {
                while (true){//来个死循环,让老板一直在这里做包子
                    //花了10秒做好了包子
                    try {
                        Thread.sleep(1000*3);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //保证等待和唤醒的线程只能有一个执行,需要使用同步技术
                    synchronized (obj){
                        System.out.println("[3秒过了包子做好了,老板准备notify();告诉一直在等待的顾客...]");
                        //notify();,唤醒顾客拿包子
                        obj.notify();
//                        obj.notifyAll();
                        System.out.println("老板:包子来嘞,总共10个包子,一共100块,拿好了您!祝您吃的开心值得愉快!");
                    }
                }
            }
        }.start();
    }
}

每次只能唤醒一个顾客。


2.void notifyAll()

/*
    进入到TIMED_WAITING状态(计时等待)有两种方式:
        1.使用sleep(long类型的毫秒值);方法,在sleep();结束后,该线程进入到RUNNABLE状态或BLOCK状态
        2.使用wait(long类型的毫秒值);方法,wait方法结束后,如果没有被notify方法唤醒,就会自动醒来,醒来的线程进入到RUNNABLE状态或BLOCK状态

    两个唤醒的方法:
        1.void notify()
          唤醒在此对象监视器上(对象锁上)等待的单个线程
        2.void notifyAll()
          唤醒在此对象监视器上(对象锁上)等待的所有线程。
 */
public class WaitAndNotify {
    public static void main(String[] args) {
        //创建锁对象,保证锁对象唯一
        Object obj = new Object();
        //创建一个顾客线程(消费者)
        new Thread(){
            @Override
            public void run() {
                while (true){//来个死循环,让顾客一直在这里等着买包子
                    //保证等待和唤醒的线程只能有一个执行,需要使用同步技术
                    synchronized (obj){
                        System.out.println("顾客1:老板,给我来10个包子,要牛肉馅儿的.");
                        //顾客调用wait方法,开始进入无限等待状态(WAITING状态)
                        //由于父类的方法没有抛异常,所以我们也不能抛,就只能用try/catch
                        try {
                            obj.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        //这是加在wait方法之后的代码,是该线程被唤醒之后要接着执行的代码
                        System.out.println("顾客1:嗯,不愧是陈记大包子,100块给你,哈哈~干饭干饭~真香啊!");
                        System.out.println("========================================================");
                    }
                }
            }
        }.start();

        //创建一个顾客线程(消费者)
        new Thread(){
            @Override
            public void run() {
                while (true){//来个死循环,让顾客一直在这里等着买包子
                    //保证等待和唤醒的线程只能有一个执行,需要使用同步技术
                    synchronized (obj){
                        System.out.println("顾客2:老板,给我来10个包子,要牛肉馅儿的.");
                        //顾客调用wait方法,开始进入无限等待状态(WAITING状态)
                        //由于父类的方法没有抛异常,所以我们也不能抛,就只能用try/catch
                        try {
                            obj.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        //这是加在wait方法之后的代码,是该线程被唤醒之后要接着执行的代码
                        System.out.println("顾客2:嗯,不愧是陈记大包子,100块给你,哈哈~干饭干饭~真香啊!");
                        System.out.println("========================================================");
                    }
                }
            }
        }.start();

        //创建一个老板的线程(生产者)
        new Thread(){
            @Override
            public void run() {
                while (true){//来个死循环,让老板一直在这里做包子
                    //花了10秒做好了包子
                    try {
                        Thread.sleep(1000*3);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //保证等待和唤醒的线程只能有一个执行,需要使用同步技术
                    synchronized (obj){
                        System.out.println("[3秒过了包子做好了,老板准备notify();告诉一直在等待的顾客...]");
                        //notify();,唤醒顾客拿包子
//                        obj.notify();
                        obj.notifyAll();
                        System.out.println("老板:包子来嘞,总共10个包子,一共100块,拿好了您!祝您吃的开心值得愉快!");
                    }
                }
            }
        }.start();
    }
}

会乱套


线程间通信

概念:多个线程在处理与同一个资源,但是处理动作(线程的任务)却不相同。

为什么要处理线程间的通信:

因为线程间需要协调通信,为什么呢?

因为当多个线程去完成一个任务时,多个线程操作同一份数据,我们希望它们是有规律的执行,这样才不会乱套,所以它们之间需要协调通信,合理分工。

为了保证效率,我们需要避免多个线程争抢同一个变量,这时候就诞生了------等待唤醒机制!

等待唤醒机制

什么是等待唤醒机制?

等待唤醒机制是为了有效的利用资源

案例分析:

案例代码:

BaoZi.java

public class BaoZi {
    //包子皮
    public String pi;
    //包子馅
    public String xian;
    //包子的状态:有-true 没有-false,设置初始值为false
    public boolean flag = false;


}

BaoZiPu.java

/*
    注意:
        包子类和包子铺类是通信关系(互斥关系)
        所以,要使用线程同步技术,保证这两个线程只能有一个在执行
        所以,锁对象必须唯一,这里就使用包子对象作为锁对象
        于是,包子铺类和顾客类就需要把包子作为对象传递进来
        于是:
            1.需要在成员变量的位置创建一个包子变量
            2.使用带参构造方法,为这个包子变量赋值

*/
public class BaoZiPu extends Thread{//这是一个线程类,所以继承Thread类
    //创建一个包子变量
    private BaoZi baoZi;

    //带参构造方法,为这个包子变量赋值

    public BaoZiPu(BaoZi baoZi) {
        this.baoZi = baoZi;
    }

    @Override
    public void run() {
        //定义一个变量,后面用来控制生产的包子种类
        int count = 0;

        //用while循环让包子铺一直生产包子
        while (true){
            //用线程同步技术,保证这两个线程只能有一个在执行
            synchronized (baoZi){
                if (baoZi.flag==true)
                //有包子,包子铺调用wait方法进行等待,等没有包子的时候再做包子
                {
                    try {
                        baoZi.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //下面是线程被唤醒之后要执行的代码,即包子没了之后要干的事儿---包子铺开始生产包子
                //包子铺开始生产包子
                //在简单枯燥的造包子吃包子过程中,我来添加一点东西---让生产的包子变成两种不一样的包子,交替生产
                if (count%2==0){
                    //生产------薄皮牛肉馅儿包子
                    baoZi.pi = "薄皮儿";
                    baoZi.xian = "牛肉馅儿";

                }else {
                    //生产------厚皮儿猪肉大葱馅儿包子
                    baoZi.pi = "厚皮儿";
                    baoZi.xian = "猪肉大葱馅儿";
                }
                //为了让count%2的值不断变换,达到交替生产两种包子的效果
                count++;
                System.out.println("包子铺正在生产"+ baoZi.pi+ baoZi.xian+"的包子。");
                //生产包子需要三秒钟
                try {
                    sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //包子生产之后,要修改包子的状态为:有包子
                baoZi.flag = true;
                //有包子之后就要唤醒顾客线程,来吃包子
                baoZi.notify();
                System.out.println("包子铺生产好了"+ baoZi.pi+ baoZi.xian+"的包子,顾客可以开吃了");

            }
        }

    }
}

运行结果:两种馅儿的包子交替生产并且交替被吃~😂😂😂


线程池

线程池是一个容器,就是一个集合(ArrayList,HashSet,LinkedList,HashMap)点击查看Java中四个常用集合的特点

底层原理:用到LinkedList这个集合

jdk1.5之后,jdk就内置了线程池,我么可以直接使用,就不用我们自己去创建线程池了


线程池的使用

RunableImpl.java

/*
    2.创建一个类,实现Runable接口,重写run方法,设置线程任务
*/
public class RunableImpl implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"新线程创建了");
    }
}

DemosThreadPool.java

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class DemosThreadPool {
    public static void main(String[] args) {
        //1.使用线程池的工厂类Executors里边提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池
        ExecutorService es = Executors.newFixedThreadPool(2);
        //3.调用ExecutorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法
        es.submit(new RunableImpl());
        //线程池会一直开启,使用完了线程,会自动把线程归还给线程池,线程可以继续使用
        es.submit(new RunableImpl());
        es.submit(new RunableImpl());

        //4.调用ExecutorService中的方法shutdown销毁线程池(不建议执行此方法,因为执行了此方法之后,线程池就会消失,导致后面的代码无法启动新的线程,就会抛异常!)
        es.shutdown();

        es.submit(new RunableImpl());//此行代码会导致程序报错,因为上一行代码销毁了线程池,所以想要再次开启新线程就会抛异常!

    }
}

运行:


posted @ 2021-04-14 12:03  牛牛ō^ō  阅读(222)  评论(2)    收藏  举报