【Java高级】一篇文章带你搞懂线程

目录

| 线程概述

| 线程创建

方式一:继承 Thread 类

方式二:实现 Runnable 接口

一些小细节

方式三:实现 Callable 接口(JDK1.8)

| 线程生命周期

生命周期概述

[获取线程信息] 方法 set/getName current

[运行 → 阻塞] 方法 sleep / suspend

[阻塞 → 就绪] 方法 interrupt / resume

[运行 → 死亡] return / 方法 stop

[给运行线程打中断标记] 三大方法

[运行 → 就绪] 方法 yield()

[线程插队] 方法 join

| 线程优先级

| 线程同步

线程同步概述

Synchronized 原理与状态

Synchronized 的使用

Synchronized 的作用范围

等待与通知 wait() notify()

应用:消费者生产者问题

| 线程高级

守护线程

定时器


| 线程概述

前言

【Java线程】是一个庞大的体系,融汇了Java、JVM、操作系统、计算机组成等多门知识的内容。它太综合了,综合到足够我们开一个专门的笔记来记录和学习它。基于此,我们在本章只能介绍一下线程中最基本的知识点,尽可能把线程的基础体系介绍清楚。至于具体的编码实现、更深入的底层原理,待到今后有需要的时候再深入理解

  • 一个进程可以执行多个线程,多线程可以提高我们程序的执行效率

  • Java程序至少有两个线程并发:一个是垃圾回收进程,一个是执行main方法的主线程

  • 使用了多线程机制之后,main方法结束,程序不一定会结束哦。因为此时只是main主线程结束了,主栈空了,其它的栈(线程)可能还在压栈弹栈(运作)

  • 两个不同的进程内存不共享。同进程下不同线程堆内存和方法区内存共享,但栈内存独立

  • 并行:时刻同时执行;并发:时间段内同时执行

| 线程创建

方式一:继承 Thread 类

编写一个类,直接继承Java.lang.Thread,重写run方法。该run方法就是一个线程

class MyThread extends Thread{
    //run不能有返回值!
	@Override
	public void run(){
		//线程代码...
	}
}



编写主类,主类Main是主线程,线程MyThread创建对象并start线程后MyThread作为支线程

public class MainClass{
	public static void main(String[] args){
		//main方法属于主线程
		
		//新建分支线程对象(线程创建了对象,但是还没启动,则仍然是单线程)
		MyThread myThread = new MyThread();
		
		//启动线程对象(start之后,该线程才算是启动了)
		myThread.start();
		
		//主线程代码...
	}
}


方式二:实现 Runnable 接口

编写一个 Runnable接口 的实现类

class MyRunnable implements Runnable{
    //run不能有返回值!
	@Override
	public void run(){
		//分支线程代码
	}
}

编写主类,创建Runnable实现类的对象r,然后创建Thread对象(以 r 为参数)

public class MainClass{
	public static void main(Strng[] args){
		//创建 Runnable接口实现类 的对象
		MyRunnable r = new MyRunnable();
		
		//创建Thread,以上述对象为参数
		Thread t = new Thread(r);
		
		//启动线程
		t.start();
	}
}

(上述两大步骤可以合为一部:使用匿名内部类)

public class MainClass{
	public static void main(Strng[] args){
		//创建Thread,以Runnable的匿名对象为参数
		Thread t = new Thread(new MyRunnable(){
            @Override
            public void run(){
                //分支线程代码
            }
        });
		
		//启动线程
		t.start();
	}
}

一些小细节

start( )方法会开辟一个新栈空间

  • start()方法的作用是:启动一个分支线程(在JVM中开辟一个新的栈空间)。

  • start()执行成功之后,就会瞬间就结束

run( )方法会自动调用,且相当于分支栈的main方法

  • 启动成功的线程会 自动 调用run方法,并且run方法在分支栈的栈底部(压栈)。(run方法在分支栈的栈底部,main方法在主栈的栈底部)

  • run和main是平级的,可以把run理解为分支栈的main方法

start和run的区别

  • satrt使用后会开辟新的栈(即开启新线程),继而自动调用run

  • 而run本身不会开辟新的栈(即相对于当前分支线程是单线程的)

  • 若主函数中的 start方法 没有执行成功,那么其后续的代码将永远不会执行

方式一 VS 方式二

  • Java是单继承的,因此方式二由于可以继承别的类,扩展性更好


方式三:实现 Callable 接口(JDK1.8)

public class MyThread{
	public static void main(String[] args){
		//创建一个FutureTask对象,重写 call 方法,该方法可以有返回值
		FutureTask task = new FutureTask(new Callable(){
			//若线程需要返回值,则使用方式三创建线程,不失为很好的方法(方式一和方式二都不能有返回值)
			@Override
			public Object call() throws Exception{
				//代码...
				return 10;
			}
		});
		
		//创建线程对象
		Thread t = new Thread(task);
		
		//启动线程
		t.start();
		
		//通过Callable接口实现的线程,若call()方法设置了返回值,则执行完毕后会返回对应的值
		int num = task.get(); //FutureTask类的非静态方法get()的作用:拿到该线程call()方法的执行结果
		//main方法若想继续往后面执行,则需要等待task线程执行完毕后,通过get()方法获取了线程结果后,才能继续往后执行
	}
}
 

| 线程生命周期

生命周期概述

  • 五种状态:创建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)

  • 线程的调度细节取决于底层OS所采用的策略

  • 主线程结束了以后,其他线程不会受影响!一旦当子线程启动以后,它就拥有和主线程一样的地位,它不会受主线程的影响

  • 线程死亡以后,不能再次调用start()方法来启动该线程,调用会返回IllegalThreadStateException异常

  • 线程的方法大致分为:对象的方法、Thread静态方法。前者针对的是特定的线程,而后者针对的是正在执行当前代码的线程【需要区分是对象还是静态方法】

[获取线程信息] 方法 set/getName current

thread对象.setName(String str);   //若不设置,则线程默认名字 -0 -1 -2 ……
thread对象.getName();   //获取线程名字
Thread nowThread = Thread.currentThread();   //【Thread类静态方法】获取当前代码段正在被哪个线程调用的那个线程对象(正在执行run方法的线程)

[运行 → 阻塞] 方法 sleep / suspend

Thread.sleep(阻塞毫秒值);   //【Thread类静态方法】阻塞正在使用当前代码段的对象
thread对象.suspend();   //【不推荐使用】使得线程进入阻塞状态,并且不会自动恢复,必须其对应的 resume() 被调用,才能使得线程重新进入可执行状态。 

[阻塞 → 就绪] 方法 interrupt / resume

阻塞的thread对象.interrupt();   //打断处于阻塞状态的线程,让其就绪。
thread对象.resume();   //【不推荐使用】恢复线程

interrupt方法用于阻塞线程:

  • 需要注意:若线程唤醒后,由阻塞→就绪,此时再使用sleep会报InterruptException 异常

  • 因此需要使用try...catch来捕获上述异常。即:使用 Thread.sleep() 需要在try内使用,捕捉 InterruptException 异常

[运行 → 死亡] return / 方法 stop

thread对象.stop();   //线程对象被终止的时候,内存有数据未保存那么数据将会丢失
//为了保证数据安全,也可以通过【标识符+return+主线程控制】来控制分支线程的存活与否

class MyThread implements Runnable{
    //成员变量布尔标记
    boolean run = true;
    
    //线程运行的run方法
    @Override
    public void run(){
		for(int i=0 ; i<100 ; i++){
            if(run){
            //若标识符run为true,则执行线程代码
            }else{
                return;  //若标识符为false,则支线程退出(return)
            }
        }
    }
}


class MainClass{
    public static void main(String[] args){
        MyThread myThread = new MyThread();
        Thread t = new Thread(myThread);
        t.start();
        //代码...
        myThread.run = false; //在主线程把分支线程的成员变量标识符改为false,则此时分支线程return 停止运行
        
        
    }
}

[给运行线程打中断标记] 三大方法

  • 实际上,interrupt不仅可以用于阻塞的线程,还可以用于运行时的线程

    被打断的线程会继续运行,但是该线程的打断标记会更新,更新为true,因此可以根据打断标记来作为判断条件使得线程停止。

  • 相较于 stop 方法直接粗暴地中断线程,这种【先标记中断,再检查是否有中断,有的话就进行对应处理】的过程,就安全很多

运行的thread对象.interrupt();   //给运行中的线程标识一个打断标记(但线程还是会继续执行)
thread对象.isInterrupted();  //查看打断标记(不会清除中断的状态标识)
thread对象.interrupted();    //查看打断标记(会清除中断状态标识)

  • 三大方法:interrupt() 标记中断标识符。isInterrupted()、interrupted() 查看打断标记(若清除标识为true,前者不会清除,而后者会)

  • interrupt方法用于运行的线程:

    • 如果一个线程在在运行中被打断,打断标记会被置为true。

    • 如果是打断因sleep wait join方法而被阻塞的线程,会将打断标记置为false

    • interrupt()方法只是改变中断状态,不会中断一个正在运行的线程。需要用户自己去监视线程的状态为并做处理。

    • this.interrupted():测试当前线程是否已经中断(静态方法)。如果连续调用该方法,则第二次调用将返回false。在api文档中说明interrupted()方法具有清除状态的功能。执行后具有将状态标识清除为false的功能。

      而 this.isInterrupted():测试线程是否已经中断,但是不能清除状态标识。

[运行 → 就绪] 方法 yield()

参考资料 yield是什么

  • 作用:提出释放CPU时间片的请求。不会释放锁,线程依然处于RUNNABLE状态,线程不会进入堵塞状态

  • 简单来说:yield方法只是提出申请释放CPU资源,至于能否成功释放由JVM决定

Thread.yield();   //【静态方法】将当前正在执行代码的线程由运行态转为就绪态(概率成功。由JVM决定)

[线程插队] 方法 join

thread对象.join();   //当前线程对象立即插队进入运行状态,待当前线程结束后,再运行之前被插队的线程

| 线程优先级

  • Java提供了一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度那个线程来执行。 线程的优先级用数字来表示,范围1~10。

  • 优先级大小

Thread.MIN_PRIORITY = 1; //最小优先级
Thread.MAX_PRIORITY = 10; //最大优先级
Thread.NORM_PRIORITY = 5; //默认优先级(main方法)

设定、获取线程优先级

thread对象.getPriority()
thread对象.setPriority(int x)
  • 优先级的设定建议在start()调度前

  • 优先级低只是意味着获得调度的概率低,并不是优先级低就不会被调用了,这都是看CPU的调度。(参见操作系统——优先级调度)

代码示例

下列代码可以说明:尽管设置了优先级,但是依然有优先级低的线程在优先级高的线程前面执行,所以优先级低只是意味着获得调度的概率低并不是优先级低就不会被调用,这都是看CPU的调度。

public class PriorityTest {

    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName()+ "-->" + Thread.currentThread().getPriority());          // Thread.currentThread().getPriority() 优先级name

        ThreadDemo thread = new ThreadDemo();

        Thread thread1 = new Thread(thread);
        Thread thread2 = new Thread(thread);
        Thread thread3 = new Thread(thread);
        Thread thread4 = new Thread(thread);
        Thread thread5 = new Thread(thread);
        Thread thread6 = new Thread(thread);

        thread1.start();
        
        thread2.setPriority(2); // 设置优先级
        thread2.start();

        thread3.setPriority(Thread.MAX_PRIORITY); // 最大优先级
        thread3.start();

        thread4.setPriority(Thread.MIN_PRIORITY); // 最小优先级
        thread4.start();

        thread5.setPriority(9);
        thread5.start();

        thread6.setPriority(7);
        thread6.start();
    }
}


| 线程同步

参考资料:

详解Synchronized关键字

关于Synchronized中this的指向是什么意思

对Synchronized的this的一些理解

Synchronized的this和.class的理解

线程同步概述

  • 异步编程模型:A与B各自执行各自的,互不干扰,互不等待。

    同步编程模型:A线程在执行的时候,B必须要等待A结束,反过来同理。即:线程间发生了等待关系(效率低,安全性高)

  • 哪些类型的数据有线程同步安全问题?只有实例变量和静态变量才会存在线程安全问题。

    局部变量永远不存在线程安全问题(因为不共享,一个线程一个栈)

  • 基于此,在开发中我们遵循的线程同步问题处理的优先级:局部变量 > 创建多个对象 > 线程同步synchronized

  • 常见集合的线程安全问题:ArrayList不安全,Vector安全,HashMap不安全HashSet不安全,HashTable安全


Synchronized 原理与状态

Synchronized的原理、四种状态

  • 原理(在Java中任何对象都有一把对象锁(本质上就是一个标记))

    • 使用Synchronized进行同步,其关键就是必须要对对象的监视器monitor进行获取,当线程获取monitor后才能继续往下执行,否则就只能等待。

    • 而这个获取的过程是互斥的,即同一时刻只有一个线程能够获取到monitor。

    • 每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一。

    • 任意线程对Object的访问,首先要获得Object的监视器,如果获取失败,该线程就进入同步状态,线程状态变为BLOCKED,当Object的监视器占有者释放后,在同步队列中得线程就会有机会重新获取该监视器。

  • 四种状态:

    • 偏向锁:减少同一线程获取锁的代价。大多数情况下,锁不存在多线程竞争,总是由同一个线程多次获得。核心思想:如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也变为偏向锁结构,当该线程再次请求锁时,无需在做任何同步操作,即获取锁的过程只需要检查Mark Word的ThreadID即可,这样就省去了大量有关锁申请的操作。显然,偏向锁不适用于锁竞争比较激烈的多线程场合。

    • 轻量级锁: 由偏向锁升级而来,偏向锁运行在一个线程进入同步块的情况写下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁。

    • 重量级锁: 若存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。


Synchronized 的使用

可以修饰的东西

  • 无论synchronized作用在方法还是对象上,如果他作用的对象是静态的,那么它取得的锁是对类,该类所有的对象一把锁

    若它作用的对象是非静态的,那么它取得的锁是对象

  • 哪些类型的数据有线程同步安全问题?只有实例变量和静态变量才会存在线程安全问题。

    基于此,在开发中我们遵循的线程同步问题处理的优先级:局部变量 > 创建多个对象 > 线程同步synchronized

  • 最后需要注意的是,synchronized在修饰object的时候,必须是不可变的对象(也就是钥匙必须唯一),否则是起不到阻塞(锁)的作用的

Synchronized语法示例

  • synchronized括号内写的是对象名 (可以是 Object(共享对象)、Class(类的所有对象)、this(线程对象))

  • 注意:Synchronized最好不要嵌套使用,否则容易死锁

  • 当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块

  • 修饰方法

//普通方法
 public synchronzied void method(){
      .....
 }

//静态方法
  public static synchronzied void method(){
       .....
 }



修饰代码块(无论如何写,其本质始终是:锁住对象!),只不过对象可以是【this(进入代码块的线程对象)】、【Object】、【Class】

//this指向当前类的对象
  synchronized(this){
    .....
  }

//怎么理解锁住this?
								 //此处this指的是进入此代码块的线程对象,如果t1进来了,那么锁住t1,若t1时间片结束了,
                    //t2走到此处也只能在上一句代码处等待t1获得了时间片后执行完synchronized锁住的所有代码,
                    //t2才能进去执行,若去掉synchronized(this),则t1和t2随时都可以进来执行此段代码中的任何一步,
                    //时间到了另一个接着进来执行
//作用于配置的实例对象
 public Object obj = new Object(); 
 synchronized(obj){
   .....
 }
 
 String lock =" ";
 synchronized(lock){
   .....
 }
//对指定类加锁(作用于类的所有对象)
  synchronized(TestClass.class){
   .....
 }
 

Synchronized 的作用范围

  • 从上面这几个实例中,我们可以看出,synchronized 在修饰不同类型数据的时候,锁的粒度(互斥范围)也是不同的(这里只说最大粒度),简单总结就是:

    方法> object > class >this

  • synchronized关键字作用在不同的对象上,影响的作用范围也不一样:

    this只会影响当前实例的线程访问;class会影响当前类所有实例的线程访问;object会影响所有访问同步代码的访问;方法会影响所有当前方法的访问


等待与通知 wait() notify()

  • wait():作用是使当前线程从调用处中断并且释放对象的锁,转入等待队列直到收到来自其他线程的notify或者notifyAll的通知才能从等待队列转入锁池队列,没有收到停止会一直死等。

  • wait(long time):相比wait多了一个等待的时间time,如果经过time(毫秒)时间后没有收到notify或者notifyAll的通知,自动从等待队列转入锁池队列

  • notify():随机从等待队列中通知一个持有相同锁的一个线程进入锁池,如果没有持有相同锁的wait线程那么指令忽略无效。注意是持有相同锁,并且是随机没有固定的,顺序这一点在生产者消费者模型中很重要,会造成假死的状态。注意,该方法不会释放对象的锁

  • notifyAll():通知等待队列中的持有相同锁的所有线程,让这些线程转入锁池队列。如果没有持有相同锁的wait线程那么指令忽略无效。

  • wait的两个方法都需要注意中断的问题,wait中断是从语句处中断并且释放锁,当再次获得锁时是从中断处继续向下执行。

  • notify 和 notifyAll方法通知是延迟通知,必须等待当前线程体执行完所有的同步方法/代码块中的语句退出释放锁才通知wait线程。

  • 这四个方法都必须在获得锁的情况下才能调用,否则会出现非法监视状态异常。

  • wait 和 notify 方法是Object的非静态方法,和Thread类无关。

  • wait搭配notify使用,作用是:无期限阻塞当前使用了该对象的线程,并且会释放之前占有的o对象的对象锁。因此,使用了wait的对象,必须用notify来进行解除阻塞!

代码示例

class MyService{
    private Object  obj = new Object();
    public void waitfun(){
        try{
            synchronized (obj){
                System.out.println("begin   wait: " + Thread.currentThread().getName() + " " + System.currentTimeMillis());
                obj.wait();
                System.out.println("end     wait: " + Thread.currentThread().getName() + " " + System.currentTimeMillis());
            }
        } catch (Exception e){
            e.printStackTrace();
        }
    }
    public void notifyfun(){
        try{
            synchronized (obj){
                System.out.println("begin notify: " + Thread.currentThread().getName() + " " + System.currentTimeMillis());
                obj.notify();
                System.out.println("end   notity: " + Thread.currentThread().getName() + " " + System.currentTimeMillis());
            }
        } catch (Exception e){
            e.printStackTrace();
        }
    }
}

class ThreadA extends Thread{
    private MyService myService;
    public ThreadA(String name, MyService myService) {
        super(name);
        this.myService = myService;
    }

    @Override
    public void run() {
        myService.waitfun();
    }
}

class ThreadB extends Thread{
    private MyService myService;
    public ThreadB(String name, MyService myService) {
        super(name);
        this.myService = myService;
    }

    @Override
    public void run() {
        myService.notifyfun();
    }
}

public class Demo {

    public static void main(String[] args) throws InterruptedException {
        MyService myService = new MyService();
        ThreadA a = new ThreadA("Thread", myService);
        a.start();
        

        Thread.sleep(1000);
        ThreadB b = new ThreadB("Thread", myService);
        b.start();
    }
}

应用:消费者生产者问题

class ThreadA{
	static Object o1 = new Object();
	
	public static void main(String[] args){
		ThreadB b = new ThreadB();
		b.start();
		sout("b启动了");
		synchronized(o1){//主线程获取o1的对象锁
			try{
				sout("等待b线程完成")
				o1.wait(1000 * 10000);//o1的对象锁释放,主线程进入等待状态
				sout("完成,现在回到主线程")
			}catch(InterruptedException e){
			
			}
		}
		sout("total =" + b.total);
	}
}

class ThreadB extends Thread{
	int total;
	@Overried
	public void run(){
		synchronized(o1){//ThreadB获取o1的对象锁
			sout("B线程正在执行");
			for(int i=0 ; i<5 ; i++){
				total += i;
				sout("total = "+total)
			}
			o1.notify(); //ThreadB释放o1的对象锁,通知其它等待o1对象的线程继续运行
		}
	}
}

| 线程高级

守护线程

  • Java的线程种类:用户线程(如main线程)、守护线程(如垃圾回收线程、每天00:00自动保存用户数据线程)

  • 守护线程可以理解为:对用户线程进行一系列的保障动作,保障用户线程的数据安全、执行顺利等,当所有用户线程结束之后,守护线程才会结束。

  • 守护线程的特征

    1. 守护线程是一个死循环

    2. 当所有的用户线程结束之后,守护线程会自动结束

  • 设置线程为守护线程

thread对象.setDaemon(true);
  • 可以选择性的人为利用sleep()来实现定时器的功能:每隔特定时间执行特定的程序。但实际上,这种做法太low了,下面介绍一下高级的东西:定时器

定时器

  • 定时器的作用是:间隔特定的时间,执行特定的程序(如:实际开发中,每隔一段时间保存一次数据),一般结合线程来用吼

  • 一般使用 Spring框架中的 SpringTask 来设置定时器

  • SpringTask框架计时器的底层原理是 Timer

public class TimerTask{
	public static void main(String[] args) throws Exception{
		//创建定时器对象
		Timer timer = new  Timer();
		
		//指定定时执行的任务
		//timer.schedule(对象 , 第一次执行的时间Date对象 , 执行间隔时间ms);
		SimpleDateFormat sdf = new SimpleDataFormate("yyyy-MM-dd HH:mm:ss");
		Date firstTime = sdf.parse("2020-03-14 09:30:00");
		timer.schedule(需要执行的线程对象 , firstTime , 1000*10);
	}
}

//定时任务
class LogTimerTask extends TimerTask{
	@Override
	public void run(){
		//需要执行的任务代码
	}
}

//写完之后,把 TimerTask里面【需要执行的线程对象】改为【new LogTimerT】即可实现定时器间隔时间执行任务的功能啦

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Graskli

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值