Java复习基础 多线程笔记

1,多线程概述
多线程是实现并发机制的一种有效手段,进程和线程一样,都是实现并发的一个基本单位,线程是比进程更小的执行单位,线程是在进程的基础上进行的进一步划分,所谓多线程是指一个进程在执行过程中可以产生多个线程,这些线程可以同时存在,同时运行,一个进程可能包含了多个同时执行的线程。
1.1 进程与线程
1,进程:
正在运行的应用程序:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,即每个进程都有着自己的堆,栈等且是互不共享的
2,线程:
进程中的一个执行路径(一段程序从执行到结束的整个过程),共享一个内存空间,线程之间可以自由切换,并发执行,一个进程最少有一个线程
线程实际是在进程的基础上进一步划分的,一个进程执行后,里面的若干执行路径又可以划分为若干个线程
1.2 线程调度
1,分时调度
所有线程轮流使用CPU 的使用权力,平均分配每个线程占用CPU 的时间
2,抢占式调度
优先让优先级高的线程使用CPU ,如果线程的优先级相同,那么会随机选择一个(线程随机性),JAVA 使用的为抢占式调度
CPU 使用抢占式调度模式在多个线程间进行着高速的切换,对于CPU 的一个核心而言,某个时刻,只能执行一个线程,而CPU 的在多个线程间切换速度相对于我们的感觉很快,看上去就是在同一时刻运行,其实,多个线程程序并不能提高程序的运行是速度,但能够提高程序运行效率,让CPU 的使用效率更高。
同步与异步& 并发与并行
同步:排队执行,效率低但安全
异步:同时执行,效率高但数据不安全
并发 :指两个或多个事件在同一个时间段内发生
并行:指两个或多个事件在同一时刻发生(同时发生)
2,多线程的实现方式
2.1 继承Thread 类
步骤
1,创建一个自定义类并继承Thread 类
2,重写run 方法,创建新的执行任务(通过thread 对象的start ()方法启动任务,一般不直接调用run 方法)
3,创建自定义类对象实例,调用start,让线程执行

package com.jj.Thread1;
//继承 Thread
public class MyThread extends Thread{
    //重写run
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("run的线程 = " + i);
        }
    }


}
class ThreadTest{
    public static void main(String[] args) {
        //创建 对象实例
        MyThread myThread = new MyThread();
        //调用start 方法
        myThread.start();
        //main 的线程
        for (int i = 0; i < 50; i++) {
            System.out.println("main 的线程 = " + i);
        }
    }
}

结果
在这里插入图片描述
看到顺序并不统一,两个线程在交替执行而且各自所占的时间不完全相同,这是线程在抢时间片,谁先抢到就执行
在这里插入图片描述

2.2 实现Runnable 接口
Runnable 接口代码

public interface Runnable {
	public abstract void run();
}

步骤
1,创建一个自定义类实现Runable 接口,并实现其抽象方法 run() 编写线程要执行的任务
2,创建自定义类对象实例
3,用Thread 类创建一个对象实例。并将第二步中的自定义对象实例作为参数传给其构造函数
4,调用Thread 类实例的start 方法执行线程

package com.jj.Thread1;

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("Runnable的 = " + i);
        }
    }
}
//test 类
class Runnabletest {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
        for (int i = 0; i < 50; i++) {
            System.out.println("main的 = " + i);
        }
    }


}

上面两种方式的比较
继承Thread 类
优点 :直接使用Thread 类中的方法,代码简单
弊端 :如果已有父类 不可用,Java 不支持多继承
**实现Runnnale 接口 **
与Thread 相比优点
1,通过创建任务,给线程分配任务实现多线程,更是个多个线程同时执行相同的任务的情况
2,可以避免单继承带来的局限性 (Java 可以实现多个接口)
3,任务和线程分离,提高程序健壮性
4,后续学到的线程池技术,它只接收Runnable 类型任务,不接收Thread 类型线程。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
实现Callable 接口
Callable 接口的代码

public interface Callable<V> {
	V call() throws Exception;
}

步骤
1,创建一个自定义类实现Callable 接口,并实现其抽象方法call() ,编写线程要执行的任务

package com.jj.Thread1;

import java.util.concurrent.Callable;

public class Mycallable implements Callable {

    @Override
    public Object call() throws Exception {
        return null;
    }
}

2,创建 FutureTask 对象,并传入第一步编写的Callable 类对象

FutureTask <Interger> future = new FutureTask<>(callable)

3,通过Thread ,启动线程

new Thread(future).start();

package com.jj.Thread1;

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class Mycallable implements Callable {

    @Override
    public Object call() throws Exception {
        for (int i = 0; i < 5; i++) {
            System.out.println("callable 的 = " + i);
        }
        return null;
    }
}
//calltest
 class CallTest{
    public static void main(String[] args) {
        Mycallable mycallable = new Mycallable();
        FutureTask futureTask = new FutureTask<>(mycallable);
        new Thread(futureTask).start();
        for (int i = 0; i < 5; i++) {
            System.out.println("main = " + i);
        }
    }
}

Runnable 与 Callable 比较
相同点:
都是接口
都可以编写多线程程序
都采用 Thread.start() 启动线程
不同点:
Runnable 没有返回值,Callable 可以返回执行结果
Callable 接口的call 允许抛出异常;Runnable 的run 不能抛出
Callable 接口支持返回执行结果。需要调用FutureTask.get()得到 ,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
3,多线程的应用实例
3.1 设置和获取线程名称

package com.jj.Thread1;

public class Myrunnable1 implements Runnable{
    //重写run
    @Override
    public void run() {
        //获取当前的线程的名字
        System.out.println(Thread.currentThread().getName());
    }
}
//获取到GetThread
class GetThread{
    public static void main(String[] args) {
        //获取到当前的线程的名字
        System.out.println(Thread.currentThread().getName());
        new Thread(new Myrunnable1()).start();
        new Thread(new Myrunnable1()).start();
        //给线程加名字方式一
        new Thread(new Myrunnable1(),"fjj").start();
        Thread thread = new Thread(new Myrunnable1());
        //方式二
        thread.setName("zyh");
        thread.start();
    }
}

结果


main
Thread-1
fjj
Thread-0
zyh

线程休眠
sleep (long millis) 是Thread 类的静态方法,类名直接调用即可,单位是毫秒

package com.jj.Thread1;

public class SleepDemo {
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            System.out.println("i = " + i);
            Thread.sleep(1000);
        }
    }
}

线程阻塞
所有较耗时的操作都能称为阻塞,也叫耗时操作
3.3 线程的中断
一个线程是一个独立的执行路径,它是否结束应该是自己自身决定的
因为线程执行过程会有很多资源需要使用或者释放,如果干涉它的结束,很可能导致资源没能来的及释放,一直占用,从而产生无法回收的内存垃圾
Java 以前提供stop ()方法可以结束线程,现在过时,现在有新的方法 interrupt 来控制它的结束
具体方法就是调用interrupt 方法,子线程执行时捕获中断异常,并在catch快中,添加处理释放资源的代码

package com.jj.Thread1;

public class SleepDemo implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i <= 10; i++) {
            System.out.println(Thread.currentThread().getName() + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                //发现中断标记会直接进去 catch快
                System.out.println(Thread.currentThread().getName() + "我自杀了");
                return;
            }

        }
    }
}
class InterrupTest{
    public static void main(String[] args)  {
        Thread thread = new Thread(new SleepDemo());
        thread.setName("fjj");
        thread.start();
        //main 线程
        for (int i = 0; i <=5; i++) {
            System.out.println(Thread.currentThread().getName() + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        thread.interrupt();
    }
}

在这里插入图片描述

3.4 守护线程
用户线程 :当一个进程不包含任何存活的用户进程时,进行结束
守护线程 :守护用户线程,当最后一个用户线程结束时,所有的守护线程自动死亡。
直接创建的都是用户线程
设置线程为守护线程,在启动之前设置,语法为 :线程对象.setDaemon(true)

package com.jj.Thread1;

public class MyRunnable2 implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("i = " + Thread.currentThread().getName()+i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
                return; //直接结束
            }
        }
    }
}
//测试类
class  MyReunnableTest{

    public static void main(String[] args) {
        Thread thread = new Thread(new MyRunnable2());
        thread.setName("fjj");
        thread.setDaemon(true);
        thread.start();
        for (int i = 0; i < 5; i++) {
            System.out.println("i = " + Thread.currentThread().getName()+i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
               
            }
        }
    }
}

在这里插入图片描述

线程安全的问题
举例子 :三个窗口 (线程)同时卖5张票

package com.jj.Thread1;
//线程安全问题  卖票问题
public class Demo implements Runnable{
    //定义5张票
    int count =5 ;
    @Override
    public void run() {

       while (count>0) {
           //卖票
           System.out.println(Thread.currentThread().getName()+"在卖票");
           //休眠
           try {
               Thread.sleep(1000);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
           count --;
           System.out.println(Thread.currentThread().getName()+"余票"
           +count);
       }
    }

    public static void main(String[] args) {
        Demo demo = new Demo();
        new Thread(demo).start();
        new Thread(demo).start();
        new Thread(demo).start();
    }
}

结果
在这里插入图片描述
看到余票出现了负数,这是不合理的,这就是线程不安全导致出现这种情况的原因:线程争抢。导致线程不安全,多线程在进行同一卖票任务时,没人干涉,各个窗口疯狂卖票,导致出现负数的情况

不安全的原因
当多线程并发临界资源的时候,如果破坏原子操作,可能会造成数据不一致。
临界资源
共享资源(同一对象),一次仅允许一个线程使用,才可保证其正确性
原子操作:不可分割的多步操作,被视作一个整体,其顺序和步骤不可打乱或缺省
解决方法:
保证一段数据同时只能被一个线程使用(排队使用),也就是线程同步,给线程加锁 (synchronized)
一共有三种方法解决不安全的问题 :同步代码快。同步方法,显示锁
方式一 同步代码块
使用 关键字 就是同步代码块
多个同步代码块如果使用相同锁对象,那么他们就是同步的
语法格式 :synchronized (锁对象) {}
任何对象都可以作为锁对象存在
修改刚的代码

package com.jj.Thread1;
//线程安全问题  卖票问题
public class Demo implements Runnable{
    //定义5张票
    private int count =5 ;
    //写在 run () 方法外,保证每个线程的锁都是一样的 
    //只有锁对象相同,线程才会排队执行
    private  final  Object o = new Object();
    @Override
    public void run() {

        while (true) {
            synchronized (o){
           if (count>0) {

               //卖票
               System.out.println(Thread.currentThread().getName()+"在卖票");
               //休眠
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               count --;
               System.out.println(Thread.currentThread().getName()+"余票" +count);
           } else {
               break;
           }

           }
        }


    }

    public static void main(String[] args) {
        Demo demo = new Demo();
        new Thread(demo).start();
        new Thread(demo).start();
        new Thread(demo).start();
    }
}

结果
在这里插入图片描述
同步方法
以方法为单位进行加锁,把synchronized 关键字修饰在方法中
接着修改上面的代码

package com.jj.Thread1;
//线程安全问题  卖票问题
public class Demo implements Runnable{
    //定义5张票
    private int count =5 ;
    private  final  Object o = new Object();
    @Override
    public synchronized void run() {

        while (true) {

           if (count>0) {

               //卖票
               System.out.println(Thread.currentThread().getName()+"在卖票");
               //休眠
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               count --;
               System.out.println(Thread.currentThread().getName()+"余票" +count);
           } else {
               break;
           }

           }
        }

		 public static void main(String[] args) {
        Demo demo = new Demo();
        new Thread(demo).start();
        new Thread(demo).start();
        new Thread(demo).start();
    }
}

直接在方法上加上 synchronized 关键字就可以实现
结果
在这里插入图片描述
显式锁
上面的都是隐式锁
自己创建一把锁

//自己创建一把锁
lock l = new ReentrantLock();
// lock 加锁 
lock()
//unlock ()  解锁
unlock()
package com.jj.Thread1;

import java.util.concurrent.locks.ReentrantLock;

//线程安全问题  卖票问题
public class Demo implements Runnable{
    //定义5张票
    private int count =5 ;
    //显示锁
    ReentrantLock lock = new ReentrantLock();
    @Override
    public  void run() {

        while (true) {
            lock.lock();
           if (count>0) {

               //卖票
               System.out.println(Thread.currentThread().getName()+"在卖票");
               //休眠
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               count --;
               System.out.println(Thread.currentThread().getName()+"余票" +count);
           } else {
               break;
           }
            lock.unlock();
           }
        }




    public static void main(String[] args) {
        Demo demo = new Demo();
        new Thread(demo).start();
        new Thread(demo).start();
        new Thread(demo).start();
    }
}

在这里插入图片描述

线程死锁
概述
当两个或两个以上的线程在执行过程中,因为争夺资源而造成的一种相互等待的状态,由于存在一种环路的锁依赖关系而永远地等待下去,如果没有外部干涉,他们将永远等待下去,此时的这个状态称之为死锁
多个线程相互占有对方的资源的锁,而又相互等对方释放锁,此时若无外力干预这些线程则一直处于阻塞的假死状态,形成死锁。
如图所示
在这里插入图片描述
死锁产生的条件:
互斥条件:指进程对所分配的资源进行排他性使用,即在一段时间内某资源只由一个进程占有,如果此时还有其他进程请求资源,则请求者只能等待,直至占有资源的进程用完释放。
请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但对自己已获得的其他资源保持不放
不剥夺条件 指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放
环路等待条件指在发生死锁时,必然存在一个进程-----资源的环形链,类似与一个集合,循环等待

如何避免死锁

1,按顺序加锁:如果每个线程都按同一个的加锁顺序这样就不会出现死锁
2,给锁加时限 :每个线程获取锁的时候加上个时限,如果超过某个时间就放弃锁
3,死锁检测:按线程间获取锁的关系检测线程间是否发生死锁,如果发生死锁就执行一定的策略,如终端线程或回滚操作等。
多线程通信
在这里插入图片描述
什么时候需要通信
多个线程并发执行时,在默认情况下Cpu 是在随机切换线程的,如果我们希望他们有规律,就可以使用通信
生产者与消费者
假设现在有 Cooker 类 ,Waiter 类 ,Food 类
厨师cooker 为生产者线程,服务员waiter 为消费者线程,食物food 为生产与消费者的物品
假设目前只有一个厨师,一个服务员,一个盘子,理想状态是:厨师生产一份菜,服务员端走一份,且饭菜的属性不能发生错乱
厨师可以制作两种口味的饭菜,制作100次
服务员可以端走饭菜100次

package com.jj.Thread1;

public class Demo1 {
    public static void main(String[] args) {
        Food food = new Food();
        new Cooker(food).start();
        new Waiter(food).start();
    }


    //厨师类 生产者
    static class Cooker extends Thread{
        private Food f;
        ///构造方法
        public Cooker(Food f){
            this.f = f;
        }
//重写 run

        public void run() {
            //生产100个菜
            for (int i = 0; i < 100; i++) {
                if (i%2==0) {
                    f.setNameAndTaste("菜1","味道1");
                } else {
                    f.setNameAndTaste("菜2","味道2");
                }
            }
        }
    }
    //服务者 Waiter  消费者
    static class Waiter extends Thread {
    private Food f;
    public Waiter (Food f) {
        this.f=f;
    }
    //重写run

        public void run() {
            for (int i = 0; i < 100; i++) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //调用 f.get()
                f.get();
            }
        }
    }
    // 生产的
    static class Food {
        //菜的名字
        private String name;
        //味道
        private String taste;
        public void setNameAndTaste(String name,String taste){		//生产
            this.name = name;			//先设定名称
            try {
                Thread.sleep(100);		//为使线效果明显,中间休眠一段时间
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.taste = taste;			//休眠后设定味道
        }
        public void get(){ //消费
            System.out.println("服务员端走的菜名称是:" + name + ",味道:" + taste);
        }



    }
}

结果
在这里插入图片描述
原因:我们在设定菜名和味道的 setNameAndTaste 方法中,先设定名称,然后休眠一段时间,在设定味道,中间休眠的那段时间很可能发生时间片丢失,所以混乱了。
解决方式一:
为了防止在生产过程中 出现时间片切换,我们可以加synchronized 关键字 修饰方法

package com.jj.Thread1;

public class Demo1 {
    public static void main(String[] args) {
        Food food = new Food();
        new Cooker(food).start();
        new Waiter(food).start();
    }


    //厨师类 生产者
    static class Cooker extends Thread{
        private Food f;
        ///构造方法
        public Cooker(Food f){
            this.f = f;
        }
//重写 run

        public void run() {
            //生产100个菜
            for (int i = 0; i < 100; i++) {
                if (i%2==0) {
                    f.setNameAndTaste("菜1","味道1");
                } else {
                    f.setNameAndTaste("菜2","味道2");
                }
            }
        }
    }
    //服务者 Waiter  消费者
    static class Waiter extends Thread {
    private Food f;
    public Waiter (Food f) {
        this.f=f;
    }
    //重写run

        public void run() {
            for (int i = 0; i < 100; i++) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //调用 f.get()
                f.get();
            }
        }
    }
    // 生产的
    static class Food {
        //菜的名字
        private String name;
        //味道
        private String taste;
        public synchronized void setNameAndTaste(String name,String taste){		//生产
            this.name = name;			//先设定名称
            try {
                Thread.sleep(100);		//为使线效果明显,中间休眠一段时间
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.taste = taste;			//休眠后设定味道
        }
        public synchronized void get(){ //消费
            System.out.println("服务员端走的菜名称是:" + name + ",味道:" + taste);
        }



    }
}

在这里插入图片描述
可以看出,依然不符合实际情况,这是因为synchronized 只确保了方法内部不会发生线程切换,但并不能保证生产一个,消费一个的逻辑关系
方式二
在解决方案一的基础上,进行线程之间的通信
厨师做完饭后喊醒服务员,自己睡,同理 服务员送我叫醒厨师,自己睡着,修改

在这里插入图片描述
在这里插入图片描述
线程的六种状态
在这里插入图片描述
线程池 Executors
普通线程的执行流程
创建线程–》创建任务–》执行任务–》关闭任务

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁擦创建线程和销毁线程需要时间,线程池就是一个容纳多个线程的容器,池中的线程可以反复的使用,省去频繁创建和销毁线程对象的操作,节省了大量的时间和资源。
线程池的好处
降低资源消耗
提高响应速度
提高线程的可管理性
Java 中有四种线程池 缓存线程池,定长线程池,单线程池,周期性任务定长线程池
缓存线程池
长度无限制
执行流程
1,判断线程池是否存在空闲线程
2,存在则使用
3,不存在则创建线程,并放入线程池,然后使用

ExecutorService service = Executors.newCachedThreadPool(); //获取缓存线程池对象
//向线程池中 加入 新的任务
service.execute(new Runnable() {
	@Override
	public void run() {
		//线程任务代码
	}
});

定长线程池
长度是指定的数值
步骤:
1,判断线程池是否存在空闲线程
2,存在则使用
3,不存在空闲线程,线程池未满的情况下则创建线程,然后使用
4,不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程。

ExecutorService service = Executors.newFixedThreadPool(2);
service.execute(new Runnable() {
	public void run() {
		//线程任务代码
	}
});

单线程池
1,判断线程池那个线程是否空闲
2,空闲则使用
3,不空闲则等待池单个线程空闲后使用

ExecutorService service = Executors.newSingleThreadExecutor();
service.execute(new Runnable() {
	public void run() {
        //线程任务代码
    }
});

周期性任务定长

步骤
1,判断线程池是否存在空闲线程
2,存在则使用
3,不存在空闲线程,且线程池未满的情况下,则创建线程,并放入线程池后使用
4,不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
周期性任务执行时,定时执行,当某个时机触发时,自动执行某任务。

ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
/**
* 定时执行
* 参数1. runnable类型的任务
* 参数2. 时长数字   5 
* 参数3. 时长数字的单位 TimeUnit.SECONDS
*/
service.schedule(new Runnable() {
	public void run() {
		//线程任务代码
	}
},5,TimeUnit.SECONDS);
/**
* 周期执行
* 参数1. runnable类型的任务
* 参数2. 时长数字(延迟执行的时长)	5
* 参数3. 周期时长(每次执行的间隔时间) 2
* 参数4. 时长数字的单位  TimeUnit.SECONDS
*/
service.scheduleAtFixedRate(new Runnable() {
	public void run() {
		//线程任务代码
	}
},5,2,TimeUnit.SECONDS);

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值