java基础之多线程

一、基本概念

**程序(program)**是为完成特定任务、用某种语言编写的一组指定的集合。即指一段静态的代码,静态对象。
**进程(process)**是程序的一次执行过程、或理解为正在运行的程序。是一个动态的过程:有它自身的产生、存在和消亡的过程——是一个周期。
》程序是静态的,进程是动态的
》进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域
**线程(thread)**进程可进一步细化为线程,是一个程序内部的一条执行路径。
》若一个进程同一时间并行执行多个线程,就是支持多线程的
》线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小
》一个进程中的多个线程共享相同的内存单元、内存地址空间—>它们从同一堆中分配对象,可以访问相同的变量和对象,这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能会带来安全的隐患。
一个java应用程序java.exe,其实至少有三个线程:main()主线程,gc()垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程

并行和并发
》并行:多个CPU同时执行多个任务。 比如:多个人同时做不同的事情。
》并发:一个CPU(采用时间片)“同时”执行多个任务。(不是真的同时,只是cpu线程间快速切换)

使用多线程的优点:

**背景:**以单核CPU为例,之使用单个线程先后完成多个任务(调用多个放),肯定比用多个线程来完成用的时间更短(单核cpu对多线程还要快速切换),为何仍需要多线程呢?
多线程序的优点:
1.提高应用的响应,对图形化界面更有意义,可增强用户体验。
2.提高计算机系统CPU的利用率
3.改善程序结构。将既长又负责的进程分为多个线程,独立运行,利于理解和修改
何时需要多线程:
1.程序需要同时执行两个或多个任务(如程序的垃圾回收也同时进行)
2.程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
3.需要一些后台运行的程序时。

二、线程的创建和使用

--------- 大体有四种方式创建

java语言的JVM运行程序运行多个线程,它通过java.lang.Thread类来体现。
Thread类的特性:
》每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主体成为线程体
》通过该Thread对象的start()放啊来启动这个线程,而非直接调用run()

JDK1.5之前的线程创建和使用

第一种创建方式:继承Thread类的方法
//1.创建一个继承Thread类的子类
    class MyThread extends Thread{
    @Override
//2.重写Thread类的run()-->将此线程执行的操作声明在run()中
   public void run() {
       //输出基数
    for(int i=0;i<100;i++) {
        if(i%2==0){
            System.out.println(i);
        }}}}
        
public class threadTest{
    public static void main(String[] args) {
//3.创建Thread类的子类对象
        MyThread t1=new MyThread();
        //通过此对象调用start();启用当前线程,调用当前线程的run()
        t1.start();

        System.out.println("hello,word!");	
        //正常来说,是hello,word在start()方法的后面执行,但  System.out.println("hello,word!");	只有一条语句,且它们两是多线程同时执行,所以也可以看到hello,word先输出。
(注意:这不是绝对的,主要看CPU的切换频率快慢)
    }
}
小贴士:调用run和调用start有什么区别
在上面代码的基础上,如果使用
t1.run()而不是t1.start();		
//这个只是单纯的创造对象调用方法,不是多线程!就是说必须是start()调用的,

解释说明:
① run( )在Thread类中是不执行操作的。被子类方法重写后调用到start方法(继承表示创建线程,调用start方法运行线程 ),一旦线程被CPU调度(eg:循环),处于运行状态,那么线程才会去调用这个run()方法(调用重写后,子类的run())

第二种创建方式:实现Runnable接口

步骤:
①.创建一个实现了Runnable接口的类
②.类去实现Runnable中的抽象方法:run()【恰好也是run方法】
③.创建实现类的对象
④.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
⑤ .通过Thread类的对象调用start()

//1.创建一个实现了Runnable接口的类
class MyThread 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 ThreadTest1 {
    public static void main(String[] args) {
//3.创建实现类对象
        MyThread mythread = new MyThread();
//4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
        Thread t1=new Thread(mythread);
//5.通过Thread类的对象调用Start()方法
        t1.start();}}		//调用主线程
	  System.out.println("hello,word!");	//同时进行的线程,也可以理解为副线程
总结上述两种创建方法:

共同点:
继承Thread类还是说继承Runnable接口,都是需要调用Thread类的start()方法
两种方式都需要重写run(),将线程要执行的逻辑声明在run()方法中
对比
----- 单继承缺陷太大了,建议使用第二种接口的方式
通过java单继承的角度,用继承Thread类有局限性;所以用实现Runnable接口更好
实现Runnable接口天然就有共享的特性,更适合处理多个线程有共享数据的情况。

JDK5.0新增线程二种创建方式

新增方式一:实现Callable接口
//1.创建一个实现Callable的实现类
class NumThread implements Callable{
    //2.实现call方法,将此线程需要执行的操作声明在call()中
    @Override
    public Object call() throws Exception {		//解释:因为重写的call(),在Callable类中用的是泛型<v>,所以要用object类型兼容
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            if(i % 2 == 0){
                System.out.println(i);
                sum += i;
				}}
        return sum;	int转成了integer,后面泛型会讲
}}

public class ThreadNew {
    public static void main(String[] args) {
        //3.创建Callable接口实现类的对象
        NumThread numThread = new NumThread();
        //4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
        FutureTask futureTask = new FutureTask(numThread);
        //5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
        new Thread(futureTask).start();     //也是要new Thread(),不然无法调用start()
//调用start,执行里面的run()方法才是主要内容
        try {
            //6.获取Callable中call方法的返回值
            //get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值;也就是说get()的作用只是得到call的return sum
            Object sum = futureTask.get();
            System.out.println("总和为:" + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }}}

与使用Runnable相比,Callable功能更强大些
》相比run()方法,可以有返回值(也就是一个线程可以在完成后给另外一个线程一个东西)
》方法可以抛出异常,被外面的操作捕获,获取异常的信息
》支持泛型的返回值
》需要借助FutureTask类,比如获取返回结果

实现Callable的步骤:

①创建类实现Callable接口;
②实现call方法,将此线程需要执行的操作声明在call()中;
③创建Callable接口实现类的对象;
④将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象;
⑤将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start();
⑥获取Callable中call方法的返回值(非必要的)

FutureTask接口:

》可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。
》FutrueTask是Future接口的唯一的实现类
》FutrueTask同时实现Runnable,Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值

新增方式二:使用线程池

背景: 经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。例如:手机刷图片
思路: 提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似:生活中的公共交通工具。

好处:

》提高响应速度(减少了创建新线程的时间)
》降低资源消耗(重复利用线程池中线程,不需要每次都创建)
》便于线程管理
√ corePoolSize:核心池的大小
√ maximumPoolSize:最大线程数
√ keepAliveTime:线程没有任务时最多保持多长时间会终止
√(设置的时候记得在前面加set,因为使用方法管理的)

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);
            }}}}

class NumberThread1 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);     //线程池里面有10个线程;Executors是工具类
        ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;         //service强制类型转换
        //如果要设置属性,就不能在ExecutorService接口中,要在实现类中设置,所以new了service1
        //注释:ThreadPoolExecutor继承AbstractExecutorService;AbstractExecutorService实现ExecutorService
        //设置线程池的属性
//        System.out.println(service.getClass());   //可以用getClass()方法查看service()是那个类,ExecutorService只是接口,
//        service1.setCorePoolSize(15);
//        service1.setKeepAliveTime();
        //2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
        service.execute(new NumberThread());//适合适用于Runnable     //匿名对象
        service.execute(new NumberThread1());//适合适用于Runnable    //相当于提供两个线程

//       service.submit(Callable callable);//适合使用于Callable
        //有返回值callable,可以用   FutureTask futureTask = new FutureTask(callable)接受一下
        //3.关闭连接池
        service.shutdown();
    }}

三、线程的调度/分类(流水帐)

1.调度策略

1.采用时间片的方式(切换,执行一段这个线程,执行下另外一个线程)
2.抢占式:高优先级的线程抢占CPU

2.java的调度方式

》同优先级线程组成先进先出队列(先到先服务),使用时间策略
》对高优先级,使用优先调度的抢占式策略

3线程的优先级:

3.1常量:
MAX_PRIORITY:10
NORM_PRIORITY:5 ---------->默认的优先级
MIN_PRIORITY:1
注意: 优先级高的线程并不代表都是比低优先级的线程先执行,还是有交互的情况的。(比较不是sleep())
**线程优先级的继承特性:**也就是如果线程A启动线程B,那么线程A和B的优先级是一样的;

说明: 高优先级的线程要抢占低优先级线程CPU的执行权,但是只是从概率上讲,高优先级的线程高概率的情况下被执行。

3.2如何获取和设置当前线程的优先级:

getpriority():获取线程的优先级
setPriority(int p):设置线程的优先级

4.线程的分类

java中的线程分为两类:一种是守护线程,一种是用户线程
它们在几乎每个方面都是相同的,唯一的区别是判断JVM何时离开。
守护线程是用来服务用户线程的,通过在start()方法前调用。
thread.setDaemon(true) 可以把一个用户线程变成一个守护线程。
java垃圾回收就是一个典型的守护线程
若JVM中都是守护线程,当前JVM将退出
形象理解就是:兔死狗烹,鸟尽弓藏

四、线程的生命周期

JDK中用Thread.State类定义了线程的几种状态
要想实现多线程,必须在主线程种创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:
新建: 当一个Thread类或其子类的对象被声明并创建时,新的线程对象处于新建状态
就绪: 处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
运行: 当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能
阻塞: 在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态
死亡: 线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
在这里插入图片描述

五、线程同步(解决线程安全问题)

当多个控制线程共享相同的内存时,需要确保每个线程看到一致的数据视图。如果每个线程使用的变量都是其他线程不会读取或修改的,那么就不会存在一致性问题。同样地,如果变量是只读的,多个线程同时读取该变量也不会有一致性问题。但是,当某个线程可以修改变量,而其他线程也可以读取或修改这个变量的时候,就需要对这些线程进行同步,以确保它们在访问变量的存储内容时不会访问到无效的数值。
在java中通过同步机制,来解决线程的安全问题

1.解决方式一:同步代码块

synchronized(同步监视器){
			需要被同步的代码
}

说明: 1.操作共享数据的代码,即为需要被同步的代码 ——>不能包含代码多了,也不能包含代码少
2.共享数据:多个线程共同操作的变量,比如:卖票这个问题中的ticket就是共享数据
3.同步监视器,俗称:锁。任何一个类的对象,都可以充当锁
要求:多个线程必须要共用同一把锁
4.注意:用同步代码块的方法解决继承Thread类的线程创建,记得把同步监视器设定为唯一(加上static)
5.补充:在实现Runnable接口创建多线程的方式中,可以考虑用this充当同步监视器,考虑使用当前类充当同步监视器。(只加载过一次的类也是对象)
(记住的一点是,同步监视器是唯一的,也就是找到唯一的对象)
代码演示:

class Window1 implements Runnable{
    private int ticket = 100;
//    Object obj = new Object();
//    Dog dog = new Dog();		这两个构造对象都是解决实现Runnable接口的安全问题
    @Override
    public void run() {
//        Object obj = new Object();	这里做监视器很明显不是唯一的
        while(true){            //注意:synchronized如果包含多了,比如包含了while(),就变成全运行只有单线程在运行了(其他窗口就没机会抢到票)
            synchronized (this){//此时的this:唯一的Window1的对象   //synchronized (dog) {
要是继承Thread类:       // synchronized (Window2.class){  	
    //Class clazz = Window2.class,	Window2.class只会加载一次
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);          //需要被同步的代码
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
                    ticket--;
                } else {
                    break;
                }}}}}

//下面的是用Runnable接口做的,继承Thread类的也是常不多
public class WindowTest1 {
    public static void main(String[] args) {
        Window1 w = new Window1();

        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

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

2.解决方式二:同步方法

写法: 在方法的类型前面加上synchronized关键字,(要注意,方法里面要是完整的共享数据代码,也就是不能包含多也不能包含少

//继承Thread类的也是差不多,自己看代码去
class Window3 implements Runnable {
    private int ticket = 100;
    @Override
    public void run() {
        while (true) {
            show();
        }}

    private synchronized void show(){//同步监视器:this
        //synchronized (this){
            if (ticket > 0) {

                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
                ticket--;
            }
        //}
    }
    //还缺少一个break
    ..............后面也是像上面那样
关于同步方法的总结:

1.同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
2.非静态的同步方法,同步监视器是:this
静态的同步方法,同步监视器是:当前类本身

同步的方式,解决了线程的安全问题。————好处
操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。——局限性

3.解决方式三lock(锁):

从JDK5.0开始,java提供了更强大的线程同步机制——通过显示定义同步锁对象来实现同步。同步锁使用lock对象。
Java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应获得Lock对象。
步骤:
①实例化ReentrantLock
②调用锁定方法Lock()
③调用解锁方法:unlock()

ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较ReentranLock,可以显示加锁、释放锁。

六、线程死锁的问题

1.死锁:

》不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的锁
》出现死锁后,不会出现异常,不会出现提示,只是所有的线程状态,无法继续
关于死锁的演示

package com.atguigu.java1;
class A {
	public synchronized void foo(B b) { //同步监视器:A类的对象:a
		System.out.println("当前线程名: " + Thread.currentThread().getName()
				+ " 进入了A实例的foo方法"); // ①
      //  sleep(方法为了增大死锁的概率)
//		try {
//			Thread.sleep(200);
//		} catch (InterruptedException ex) {
//			ex.printStackTrace();
//		}
		System.out.println("当前线程名: " + Thread.currentThread().getName()
				+ " 企图调用B实例的last方法"); // ③
		b.last();
	}

	public synchronized void last() {//同步监视器:A类的对象:a
		System.out.println("进入了A类的last方法内部");
	}
}

class B {
	public synchronized void bar(A a) {//同步监视器:b
		System.out.println("当前线程名: " + Thread.currentThread().getName()
				+ " 进入了B实例的bar方法"); // ②
//		try {
//			Thread.sleep(200);
//		} catch (InterruptedException ex) {
//			ex.printStackTrace();
//		}
		System.out.println("当前线程名: " + Thread.currentThread().getName()
				+ " 企图调用A实例的last方法"); // ④
		a.last();
	}

	public synchronized void last() {//同步监视器:b
		System.out.println("进入了B类的last方法内部");
	}
}

public class DeadLock implements Runnable {
	A a = new A();
	B b = new B();
	public void init() {
		Thread.currentThread().setName("主线程");
		// 调用a对象的foo方法
		a.foo(b);
		System.out.println("进入了主线程之后");
	}
	public void run() {
		Thread.currentThread().setName("副线程");
		// 调用b对象的bar方法
		b.bar(a);
		System.out.println("进入了副线程之后");
	}
	public static void main(String[] args) {
		DeadLock dl = new DeadLock();
		new Thread(dl).start();		//匿名对象,执行线程b
//解释:如果不传参到构造器,DeadLock类是调不了start()
/*也可以这样理解:Thread t2=new Thread(dl);	t2.start();*/
		dl.init();		//调用主线程(线程a)
	}
}

打印结果:

当前线程名: 主线程 进入了A实例的foo方法
当前线程名: 主线程 企图调用B实例的last方法
当前线程名: 副线程 进入了B实例的bar方法
当前线程名: 副线程 企图调用A实例的last方法

2.解决方法:

》专门的算法、原则
》尽量减少同步资源的定义
》尽量避免嵌套同步

七、线程的通信

线程的通信也可以叫做线程间交流的方式进行
涉及到的三个方法:
wait(): 一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器
notify(): 一旦执行此方法,就会唤醒wait()的一个线程。如果有多个线程被wait(),就唤醒优先级高的线程
notifyAll():一旦执行此方法,就会唤醒所有被wait()的线程
注意:notify()只是具有唤醒(wait的对象),而wait()才是具有释放锁(synchronized())的功能 别搞混了!!
说明:
1.wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。
2.wait(),notify(),notifyAll()三个方法的调用者必须是同步代码或同步方法中的同步监视器
否则,会出现IllegalMonitorStateException异常。
3.wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中。
理由:既然任何唯一的对象都可以当同步监视器,对象又要可以调用到这三个方法,所以这三个方法实在object类中,保证到可以调用
联想下,sleep()的调用:Thread.sleep(1000);

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值