基础知识进阶——线程

线程举例:
String家族:
StringBuilder线程非同步,不安全,但是效率高。
StringBuffer线程同步,安全性高

复习总结:
1.进程和线程:操作系统中运行的每一个任务对应一个进程,当一个程序进入内存运行时,即变成了一个进程。进程是操作系统进行组员分配和调度的一个独立单元。
线程是进程的执行单元。
2.线程的状态:新生态;就绪态;运行态;阻塞态;死亡态
3.start和run方法:让线程开始新的任务的时候,我们调用start方法,让cpu自己执行run方法,只需要调用start方法告诉cpu自己已经准备好了。
4.创建线程的两种方法:
继承Thread类。重写run方法
实现Runnable接口创建线程
5.sleep()方法:使当前线程让出cpu,留出一定时间给其他线程执行的机会。
注意调用sleep方法时,要处理异常
6.设置线程的优先级setPriority:
设置优先级只是说让这个线程可以抢到cpu时间片的概率增大,但是不意味着一定可以抢到cpu时间片,值是[0,10],默认是5
7.线程礼让:指当前运行状态的线程释放自己的cpu资源,由运行状态回到就绪状态,然后和另一个线程重新开始一起抢夺cpu时间片,谁抢到还不一定
8.临界资源抢夺问题:多个线程共享的资源成为临界资源。
多线程同时去访问临界资源的时候,可能会出现问题。
9.为了解决临界资源问题,出现了锁。当一个线程在访问临界资源的时候,上一把锁,其他线程先等待,前一个线程释放锁以后,其他线程才可以操作临界资源。
锁synchronized有两种方法:
同步代码块:在代码块上增加synchronized 修饰符
同步方法:类方法增加synchronized 修饰符
10.死锁
11.死锁解决办法使用wait方法,唤醒wait使用notify和notifyAll方法

一. 线程的基础

1.几个重要的概念:

程序:可以理解为是一组静态的代码
进程:正在进行的程序,静态的代码 运行起来
线程:正在执行程序中的小单元
线程是进程中的小单元。

2.举个例子理解一下:

聚餐
聚餐之前, 班级大扫除。
扫除需要分配任务,任务写在纸上,列一个清单。这是一个静态的。
一声令下,开始扫除,班级扫除这件事就是-----进程。
每一个同学都做自己的事情,并发执行,互相不影响。

3.如何在Java中创建线程: 让线程执行,多线程

掌握每一个线程的几种不同状态,及状态之间如何切换:
基础知识进阶——线程

如何实现线程
自定义一个类
继承Thread 实现Runnable
重写run方法
调用start方法 让线程进入就绪状态 需要注意的是start方法Thread类中

4.实现线程的过程

1.自己描述一个类
2.继承父类Thread
3.重写run方法
4.new一个线程对象 调用start()方法 让线程进入就绪状态

实现一个跑步的小例子
多个人同时跑步:苏炳添;博尔特 ;加特林

//一个继承线程的类
public class RunningMan extends Thread {
    private String name;
    
    //构造方法,两个,想用哪个用哪个。
    public RunningMan(){}
    public RunningMan(String name){
        this.name=name;
    }
    
    //重写run方法
    public void run(){
        for(int i=1;i<=100;i++){
            System.out.println(this.name+"跑到第"+i+"米啦");
        }
    }
}

主方法测试一下:

public class TestMain {
    public static void main(String[] args){
        //1.创建一个线程对象
        RunningMan r1 = new RunningMan("苏炳添");
        RunningMan r2 = new RunningMan("博尔特");
        RunningMan r3 = new RunningMan("加特林");
        //2.调用start方法  让线程进入就绪状态  按顺序逐个执行
        r1.start();//从Thread类中继承过来的方法
        r2.start();
        r3.start();
        //如果掉run,那就是单线程了,按照顺序执行了。
        //我们调用stat是告诉线程,我们准备好了,让cpu自己run。
    }
}

实现过程很简单,但是有没有什么问题?

Java中 继承,单继承
extends
Person extends Animal
我想让人去多线程,但是发现java是单继承,人已经继承了动物,就不能再继承线程了,那咋么去多线程呢?所以有第二种,多线程。

5.实现线程的另一种方法过程

1.自己描述一个类
2.实现一个父接口Runnable
3.重写run方法
4.new一个线程对象 需要创建Thread将自己的对象包起来 然后调用start()

public class RunningMan implements Runnable {
    private String name;
    public RunningMan(){}
    public RunningMan(String name){
        this.name=name;
    }
    //重写run方法
    public void run(){
        for(int i=1;i<=100;i++){
            System.out.println(this.name+"跑到第"+i+"米啦");
        }
    }
}

主方法

public class TestMain {
    public static void main(String[] args){
        //1.创建一个线程对象
        RunningMan r1 = new RunningMan("苏炳添");
        RunningMan r2 = new RunningMan("博尔特");
        RunningMan r3 = new RunningMan("加特林");
        //2.调用start方法  让线程进入就绪状态  按顺序逐个执行
        //我们必须调用start,但是不是线程没有start,所以我们构建一个线程对象。
        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r2);
        Thread t3 = new Thread(r3);
        t1.start();
        t2.start();
        t3.start();
   }
}

二. 线程之生产消费者模型

1.线程并发访问时,可能会产生线程安全问题

生产消费者模型。生产者,生产产品存入仓库,消费者消费产品,去仓库中取走产品。需要三个类,仓库,消费者以及生产者。
想验证一下,因为消费者是多线程访问仓库,是否会发生抢夺资源的现象发生。

仓库类:

public class Warehouse { 

    //仓库里面的集合  存放元素
    private ArrayList<String> list = new ArrayList<>();
    
    //向集合内添加元素的方法
    public void add(){
        if(list.size()<20) {
            list.add("a"); //仓库元素够20个,就不再往里面存元素了。
        }else{
            return;//让方法执行到这里就结束方法     
         }      
     }
     
    //从集合内获取元素的方法
    public void get(){
        if(list.size()>0) {
            list.remove(0); //拿走元素
        }else{
            return;//集合越界的问题
        }
     }
     
}

生产者类:因为生产者是多线程生产的,所以要继承Thread。

public class Producer extends Thread {

    //为了保证生产者 和消费者使用同一个仓库对象  添加一个属性
    private Warehouse house;
    public Producer(Warehouse house){
        this.house=house;
    }
    
    //生产者的run方法  一直向仓库内添加元素
    public void run(){
    //方法重写,要求名字和参数列表必须一致,所以不能传参,所以我们可以考虑将house放到属性里。
        while(true){ //死循环,一直往里放
            house.add(); 
            System.out.println("生产者存入了一件货物");
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

消费者:

public class Consumer extends Thread{

    //为了保证生产者 和消费者使用同一个仓库对象  添加一个属性
    private Warehouse house;
    public Consumer(Warehouse house){
        this.house=house;
    }
    
    //消费者的方法  一直从仓库内获取元素
    public void run(){
        while(true){
            house.get();
            System.out.println("消费者拿走了一件货物");
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

多线程并发很有可能两个都来拿元素,很有可能发生资源抢劫。
基础知识进阶——线程
右下角这种方式可能会产生线程安全问题,只剩下一个库存了,两个消费者恰好判断出仓库有文件,又同时一起取了文件,但是只有一个文件,所以会出现异常。也就是线程不安全。

写一个主方法测试一下:

public class TestMain {
    public static void main(String[] args){
        Warehouse house = new Warehouse();//里面有一个ArrayList线程非安全
        Producer p = new Producer(house);
        Consumer c1 = new Consumer(house);
        Consumer c2 = new Consumer(house);
        //以上三个线程
        p.start();
        c1.start();
        c2.start();
    }
}

运行时,会发生异常。说明刚才的分析没有错误。多线程并发,会发生安全问题。

通过这个模型 成功的演示出了 线程安全的问题
两个消费者 同时访问同一个仓库对象 仓库内只有一个元素的时候
两个消费者并发访问 会有可能产生抢夺资源的问题

2.自己解决一下线程安全的问题

  • 让仓库对象被线程访问的时候 仓库对象被锁定;
  • 仓库对象只能被一个线程访问 其他的线程处于等待状态。
  • 使用特征修饰符synchronized,也称之为线程安全锁 , 同步 , 一个时间点只有一个线程访问。
  • 两种形式写法:
    1.将synchronized关键字 放在方法的结构上
    public synchronized void get(){}
    锁定的是调用方法时的那个对象
    2.将synchronized关键字,放在方法(构造方法 块)的内部
public void get(){
   好多代码
   synchronized(对象,写this就行了){
    好多代码  //只有这句比较重要,只有执行这块代码的时候才锁定对象。
   }
   好多代码
     }

所以修改仓库类:

public class Warehouse {
    //单例设计模式
    //仓库里面的集合  存放元素
    private ArrayList<String> list = new ArrayList<>();
    //向集合内添加元素的方法
    public synchronized void add(){
        if(list.size()<20) {
            list.add("a");
        }else{
            //return;//让方法执行到这里就结束方法
        }
    }
    //从集合内获取元素的方法
    public synchronized void get(){
        if(list.size()>0) {
            list.remove(0);//集合越界的问题
        }else{
            //return;
            try {
                this.notifyAll();
                this.wait();//仓库对象调用wait  不是仓库对象等待  访问仓库的消费者线程进入等待状态
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

3.我们觉得return不是很好

对于生产者,生产发现超过20,不是不在生产了,而是等一会,一会在继续生产,所以直接return不好。
对于消费者,发现数量为0 的时候,也不是不再取了,而是等一会,一会再取,所以return不好。

  • 应该让线程的不同状态来回切换
    执行 等待 执行 等待
  • wait(); Object类中的方法
    对象.wait(); 不是当前的这个对象wait
    是 访问当前这个对象的线程wait
    将return换成如下的代码:
		try {
              this.wait();//仓库调用wait 不是仓库对象等待  访问仓库的生产者线程进入等待状态
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

但是单纯这样,会产生一个类似假死状态
所有的线程都进入等待状态 没有线程做事。所以可以使用下面的notify方法。

  • notify和notifyAll Object类中的方法
    代码为:
       try {
                this.notifyAll();
                this.wait();//仓库调用wait 不是仓库对象等待  访问仓库的生产者线程进入等待状态。
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

但是唤醒是将消费者和生产者都唤醒了,生产者和消费者哪一个先开始,我们还是不确定,我们是希望生产者先开始,但是这不是我们能决定的。所以考虑设置优先级,如下:

  • p.setPriority(10); p.getPriority();
public class TestMain {
    public static void main(String[] args){
        Warehouse house = new Warehouse();//里面有一个ArrayList线程非安全
        Producer p = new Producer(house);
        //设置线程的优先级别1-10
        p.setPriority(10); //给生产者级别高些,优先抢资源。
        Consumer c1 = new Consumer(house);
        Consumer c2 = new Consumer(house);
        p.start();
        c1.start();
        c2.start();
    }
}

笔试题

  • 程序 进程 线程 概念的区别
  • 线程的创建方式
  • 线程的几种状态 如何切换
  • sleep方法 ; wait方法的区别:
    1.类: Thread类 ;Object类
    2.调用: 静态 类名. ; 对象.
    3.理解: 哪个位置调用 哪个线程等待 ;对象调用方法 访问对象的其他线程等待
    4.唤醒:不需要别人 ;需要其他对象调用notify唤醒
    5.锁:不会释放锁 ;等待后会释放锁

三、join方法&死锁&Timer

1.join方法:

设计一个模型
1.有两个线程 One Two two加入到one里面
2.设计模型时候 two线程在one的run里面创建 保证两个有先后顺序
3.two.join(); 无参数0 有参数2000

A跑几步,B跑几步,然后遇到独木桥,然后让B先过独木桥。

基础知识进阶——线程
但是,我们以前只能让线程进入就绪状态,不能控制线程的顺序,现在,我们想让A加入到B线程中,怎么做呢?

线程一:

public class ThreadOne extends Thread {
    public void run(){
        System.out.println("thread-one start");
        ThreadTwo two = new ThreadTwo();
        two.start();//一线程运行的时候,二线程才创建
        //所以一线程应该执行的比二线程早,控制了顺序。
        try {
            two.join();//线程2加入线程1里面
            //如果填一个时间,那么一线程就等这么长时间,就执行了,就不管二线程是否执行完毕了
          //  two.join(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }//一线程等待二线程,二线程休息了5000ms
        System.out.println("thread-one end");
    }
}

线程二:

public class ThreadTwo extends Thread {
    public void run(){
        System.out.println("thread-two start");
         try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("thread-two end");
    }
}

所以主方法

public class TestMain {
    public static void main(String[] args){
        ThreadOne one = new ThreadOne();
        one.start();
    }
}

结果:
thread-one start
thread-two start
thread-two end
thread-one end

在two执行的过程中,one等待的过程中,three将two对象锁定,那么one没能推two,one会怎样呢?会不会一直在那一直等待呢?
Three:

public class ThreadThree extends Thread{

    private ThreadTwo two;
    public ThreadThree(ThreadTwo two){
        this.two=two;
    }
    
    public void run(){
        //在two执行的过程中  one等待的过程中   three将two对象锁定
        System.out.println("thread-three start");
        
        synchronized(two){
            System.out.println("two is locked");
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("two is free");
        }
        System.out.println("thread-three end");
    }
}

整体过程,one线程先执行,one线程执行的时候建立了two线程,然后遇到独木桥,one让two先执行,two.join(2000),one在这数数,2000ms。two执行需要5000ms,two还没有执行完毕,one的2000ms已经数完了,在one想要推two的时候,发现two被three拿走了锁定了,tow被three老鹰叼走了。所以one没能推two,one就在这一直等着,等到two回来,然后推two一把,才能继续执行。
线程二:

public class ThreadTwo extends Thread {
    public void run(){
        System.out.println("thread-two start");
        ThreadThree three = new ThreadThree(this);
        three.start();
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("thread-two end");
    }
}

结果:
one 启动
two 启动
three 启动
two就join啦
2000之后 one想要将two从自己的线程内剔除
发现 two对象不在自己的手里 因为对象被three线程锁定啦 10000
one只能等待 threee将two对象释放后 才能踢掉。
结果:

thread-one start
thread-two start
thread-three start
thread-two locked
thread-two end   two虽然被叼走了,但是实际上还在执行,所以他先执行完毕了因为只需要5000ms就能执行完毕,二别锁定10000ms,所以two可以执行完毕
two is free
thread-three end
thread-one end

synchronized锁 非常的厉害,
一旦对象被锁定 不释放的情况下 其他的对象都需要等待,
有可能会产生一个死锁的效果。

2.死锁

基础知识进阶——线程
一个小箱子,有俩个元素。两个人都想去拿东西,而且拿两个。可能一人拿了一个,想等着对方释放,但是对方又不释放,产生的现象称之为死锁。

死锁的效果
模拟一个模型 演示死锁。哲学家就餐的问题,上图左侧的桌子。有可能产生死锁,但是也有可能有的人快,就拿到了一双。

筷子只有属性:

public class Chopstick {
    private int num;
    public Chopstick(int num){
        this.num=num;
    }
    public int getNum(){
        return this.num;
    }
}

哲学家:

public class Philosopher extends Thread{

    private String pname;//哲学家的名字
    private Chopstick left;
    private Chopstick right;
    //private long time;
    
    public Philosopher(String pname,Chopstick left,Chopstick right,long time){
        this.pname = pname;
        this.left = left;
        this.right = right;
    }
    
    public void run(){
            synchronized (left) {
            System.out.println(this.pname+"拿起了左手边的"+this.left.getNum()+"筷子");
            synchronized (right){
                System.out.println(this.pname+"拿起了右手边的"+this.right.getNum()+"筷子");
                System.out.println(this.pname+"开始狼吞虎咽的吃起来啦");
            }
        }
    }
}

主方法:

public class TestMain {
    public static void main(String[] args){
    
        Chopstick c1 = new Chopstick(1);
        Chopstick c2 = new Chopstick(2);
        Chopstick c3 = new Chopstick(3);
        Chopstick c4 = new Chopstick(4);
        
        Philosopher p1 = new Philosopher("哲学家a",c2,c1);
        Philosopher p2 = new Philosopher("哲学家b",c3,c2);
        Philosopher p3 = new Philosopher("哲学家c",c4,c3);
        Philosopher p4 = new Philosopher("哲学家d",c1,c4);
        
        p1.start();
        p2.start();
        p3.start();
        p4.start();
    }
}
//有可能死锁,一人拿到了一支筷子,但是也有可能不产生死锁问题,有人拿的快,吃到了饭。

想要解决死锁的问题:
1.礼让---->产生时间差
2.不要产生对象公用的问题

哲学家类:

public class Philosopher extends Thread{
    private String pname;//哲学家的名字
    private Chopstick left;
    private Chopstick right;
    private long time; //上来先睡一会儿
    public Philosopher(String pname,Chopstick left,Chopstick right,long time){
        this.pname = pname;
        this.left = left;
        this.right = right;
        this.time = time;
    }
    public void run(){
        try {
            Thread.sleep(time);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (left) {
            System.out.println(this.pname+"拿起了左手边的"+this.left.getNum()+"筷子");
            synchronized (right){
                System.out.println(this.pname+"拿起了右手边的"+this.right.getNum()+"筷子");
                System.out.println(this.pname+"开始狼吞虎咽的吃起来啦");
            }
        }
    }
}

主方法:

public class TestMain {
    public static void main(String[] args){
        Chopstick c1 = new Chopstick(1);
        Chopstick c2 = new Chopstick(2);
        Chopstick c3 = new Chopstick(3);
        Chopstick c4 = new Chopstick(4);
        Philosopher p1 = new Philosopher("哲学家a",c2,c1,0);
        Philosopher p2 = new Philosopher("哲学家b",c3,c2,3000);
        Philosopher p3 = new Philosopher("哲学家c",c4,c3,0);
        Philosopher p4 = new Philosopher("哲学家d",c1,c4,3000);
	//对面的人可以同时吃,所以可以设置睡觉时间相同。

        p1.start();
        p2.start();
        p3.start();
        p4.start();
    }
}
上一篇:Java中List循环遍历的时候删除当前对象(自己)


下一篇:Leetcode NO.21 Merge Two Sorted Lists && 合并链表(有序)