多线程(可见性、有序性、原子性、进程、线程、守护线程)、线程的生命周期、多线程的实现方式

基本概念

可见性

    是指当一个线程修改了共享变量的值,其他线程也能够立即得知这个通知。主要操作细节就是修改值后将值同步至主内存(volatile 值使用前都会从主内存刷新),除了 volatile 还有 synchronize 和 final 可以保证可见性。同步块的可见性是由“对一个变量执行 unlock 操作之前,必须先把此变量同步会主内存中( store、write 操作)”这条规则获得。而 final 可见性是指:被 final 修饰的字段在构造器中一旦完成,并且构造器没有把 “this” 的引用传递出去( this 引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到“初始化了一半”的对象),那在其他线程中就能看见 final 字段的值。

public class Test {
    public static void main(String[] args) throws InterruptedException {
        ThreadTest test = new ThreadTest();
        test.start();
        /**
         * 每次线程访问访问资源的时候都是会复制一份资源的副本,
         * 在对资源进行修改的时候才会把修改的结果也同步到原始数据上。
         * 但是在主线程这里新建test对象的时候已经复制了一份副本 0 ,而且在
         * 主线程上没有进行修改操作,所以不管test线程怎么操作数据
         * test.data永远是0
         */
        while (true) {
            if (test.data > 0) {
                System.out.println(test.data);
                break;
            }
        }
    }
}
class ThreadTest extends Thread {
    public int data = 0;
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + data++);
        }
    }
}

有序性

    即程序执行的顺序按照代码的先后顺序执行。有序性对应的就是Java的指令重排序。
    重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段,但是不管怎么重排序,单线程下程序的执行结果不能被改变。比如:a=1;b=a;由于第二个操作依赖于第一个操作,所以在编译时和处理器运行时这两个操作不会被重排序;a=1; b=2; c=a+b;第一步和第二步不存在数据依赖关系可能会发生重排序,因为需要保证最终的结果一定是c=a+b=3,所以c=a+b这个操作是不会被重排序的。
    重排序在单线程下一定能保证结果的正确性,但是在多线程环境下可能会影响结果。
    Java内存模型中的有序性可以总结为:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”和“工作内存主主内存同步延迟”。

public class Test {
    public static void main(String[] args) {
        Data data = new Data();
        new Thread(() -> {
            data.a = 1;
            data.b = 2;
        }).start();
        /**
         * 在新开辟的线程上,两个代码执行的先后顺序没有任何影响
         * 但是如果是在新线程执行第一句代码之后,主线程就抢到cpu
         * 的情况下,新线程的执行顺序对主线程是有影响的。
         */
        System.out.println(data.b);
    }
}
class Data {
    int a;
    int b;
}

原子性

    由 Java 内存模型来直接保证的原子性变量操作包括 read、load、assign、use、store 和 write。大致可以认为基本数据类型的操作是原子性的。同时 lock 和 unlock 可以保证更大范围操作的原子性。而 synchronize 同步块操作的原子性是用更高层次的字节码指令 monitorenter 和 monitorexit 来隐式操作的。
    原子操作即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。具有原子性的变量同一时刻只能有一个线程来对它进行操作,也就是说在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性的。例如 a=1是原子性操作,但是a++和a+=1就不是。
    Java中通过锁和循环CAS来实现原子性操作,比如自旋的实现思路就是循环进行CAS操作直到成功为止。
    Java中的原子性操作包括:

  • 基本类型的读取和赋值操作,且赋值必须是常量赋值给变量,变量之间的相互赋值不是原子性操作。
  • 所有引用reference的赋值操作
  • java.concurrent.Atomic.* 包中的一切操作
public class Test {
    public static void main(String[] args) throws InterruptedException {
        ThreadTest test = new ThreadTest();
        test.start();
        test = new ThreadTest();
        test.start();
        TimeUnit.SECONDS.sleep(1);
        /**
         * 结果并不绝对是 200000
         * 因为 data = data + 1;不是原子操作,可以分为1.计算 2.赋值两步
         * 刚开始线程1处于运行状态,data = 0,
         * data + 1 = 1; 然后要执行 data = 1 的时候线程2获得cpu资源
         * 由于线程1并没有完成修改操作,所以线程2获得的data依旧是0,在线程2执行
         * 若干操作时,线程1重新获得了cpu,线程1就会继续执行 data = 1 的赋值操作,
		 * 这样一来线程2做的操作就没有效果了。
         */
        System.out.println(ThreadTest.data);
    }
}
class ThreadTest extends Thread {
    public static int data = 0;
    @Override
    public void run() {
        for (int i = 0; i < 100000; i++) {
            data = data + 1;
        }
    }
}

进程

    进程是程序的一次静态态执行过程,由3部分组成cpu,data,code。程序一旦运行就是进程,进程可以看作是程序执行的一个实例。
    系统在运行的时候会为每个进程分配不同的内存区域,如果想要进程间通信,借助管道,文件等。在操作系统中能同时运行多个进程,进程间的切换会有较大的开销。
    每个进程都是独立的,一个进程崩溃后,在保护模式下不会对其它进程产生影响。

线程

    指的是进程中一个单一顺序的控制流,又被称为轻量级进程,是操作系统能够进行运算调度的最小单位。
    每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口,线程不能够独立执行,必须依存在进程中。
    线程切换的开销小。

守护线程

    Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程)。用户线程即运行在前台的线程,而守护线程是运行在后台的线程,比如垃圾回收线程就是一个守护线程。
    当用户线程都已经退出运行时,JVM就会退出,因为没有如果没有了被守护者,也就没有继续运行程序的必要了。
    要确保守护线程在其他所有非守护线程消亡时,不会由于它的终止而产生任何危害。因为在所有的用户线程退出运行前,无法判断守护线程是否已经完成了预期的任务,因此不要在守护线程中执行业务逻辑操作(比如对数据的读写等)。
设置守护线程
    用户可以用Thread的setDaemon(true)方法设置当前线程为守护线程。

  • setDaemon(true)必须在调用线程的start()方法之前设置,否则会抛出IllegalThreadStateException异常。
  • 在守护线程中产生的新线程也是守护线程。
  • 可以通过isDaemon()方法查询线程是否为守护线程。

联系和区别

    一个进程中可以有多个线程(没有线程的进程是可以看作是单线程),多个线程共享进程的堆和方法区资源,每个线程有自己的程序计数器和栈区域。
    如果一个进程内拥有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的。
    如果线程结束,进程并不一定结束;进程结束,线程都将结束。

线程的生命周期

  • 新生状态:使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态,此时它已经有了相应的内存空间和其他资源,但还处于不可运行状态。
  • 就绪状态:当线程对象调用了start()方法之后或者从阻塞、等待或睡眠状态回来后,该线程就进入就绪状态。处于就绪状态线程具备了运行条件,但还没分配到 CPU,处于线程就绪队列,等待系统为其分配 CPU,该动作称为“CPU 调度”。
  • 运行状态:当就绪状态被调用并获得处理器资源时,线程就进入了运行状态。此时将自动调用该线程对象的 run() 方法。
  • 阻塞状态:如果一个线程失去所占用资源之后,该线程就从运行状态进入阻塞状态。只有当引起阻塞的原因消除时,如睡眠时间已到,或者成功获得锁,或等待的I/O 设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续执行。
  1. 线程执行 wait() 方法,使线程进入到等待阻塞状态。
  2. 线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
  3. 调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。
  • 死亡状态:死亡状态是线程生命周期中的最后一个阶段。进入这个状态的原因有三个:
  1. 线程完成了它的全部工作.
  2. 线程被终止。
  3. 线程抛出未捕获的异常。

多线程的实现方式

继承Thread类

    单独调用run()就跟调用普通方法是一样的。
    Java 中的类是单继承的,一旦继承了 Thread 类,就不允许再去继承其它的类。
    继承Thread类编写简单,而且在run()方法内获取当前线程直接使用this就可以了,无须使用Thread.currentThread()方法;

public class MyThread extends Thread {
	@Override
	public void run() {
		System.out.println("MyThread");
	}
	public static void main(String[] args) {
		MyThread myThread=new MyThread();
		//启动线程,系统会开启一个新的线程,进而调用run()方法来执行任务
		myThread.start();
	}
}

实现Runnable接口

public class MyRunnable  implements Runnable{
	@Override
	public void run() {
		System.out.println("MyRunnable");
	}
	public static void main(String[] args) {
		MyRunnable myRunnable=new MyRunnable();
		//通过Thread类的start()方法启动线程
		Thread t=new Thread(myRunnable);
		t.start();
	}
}

实现Callable接口

    可以有返回值。

public class MyCallable implements Callable<String> {
    @Override
    public String call() {
        return "MyCallable";
    }

    public static void main(String[] args) {
        //创建任务
        MyCallable call = new MyCallable();
        //任务管理
        FutureTask<String> futureTask = new FutureTask<>(call);
        //创建代理类并启动线程
        Thread thread = new Thread(futureTask);
        thread.start();
        //call方法因为缓存只会执行一次
        new Thread(futureTask).start();
        try {
        	//会被阻塞,放在最后一行
            String result = futureTask.get(); //MyCallable
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

FutureTask

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值