Java并发的那些事儿(二)之Java并发程序基础

149 阅读7分钟

这是我参与11月更文挑战的第12天,活动详情查看:2021最后一次更文挑战

上下文切换

对于单核cpu来说,cpu在一个时刻只能运行一个线程,当在运行一个线程的过程中转去运行另一个线程,这个叫做上下文的切换。

当进行上下文切换的时候,可能上一个线程的任务并没有执行完毕,所以在切换的时候必须保存线程的运行状态,以便下次重新切换回来时能够继续切换之前的状态运行。JVM的做法是利用JMM内存模型,将执行的指令地址保存在程序计数器中,同时为保证不丢失上一次执行时变量的值,因此也需要将CPU寄存器的状态记录。所以一般来说,线程上下文切换过程中会记录程序计数器、CPU寄存器状态等数据。

对于线程的上下文切换实际上就是 存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行。

因此,虽然说多线程能很好地提升执行效率,但由于线程之间的切换也会带来一定的开销代价,并且导致多个线程占有的资源数增加,所以在多线程编程时也需要考虑到这些因素。

Java并发程序基础

1. Thread类的使用

1). start方法与run方法

start() 用来启动一个线程,当调用start方法后,系统才会开启一个新的线程来执行用户自定义的子任务。需要注意的是,当新建了一个线程后,不会立即进入就绪状态,因为在这之前线程还需要被jvm分配一定的内存空间,当分配好了,线程有了运行的条件才会进入就绪状态

run() 方法是线程启动后执行的方法,即当调用start() 启动线程后才会执行run的方法体,不需要我们主动去调用。(注意:继承Thread类必须重写run方法,在run方法中定义具体要执行的任务)

2). sleep()、wait()与notify()

sleep相当于让线程睡眠,交出CPU,让CPU去执行其他的任务。但需注意的是sleep方法并不会释放锁,即当前线程持有某个对象的锁,则使用sleep方法后其它线程也无法访问到该对象。

这里说一下Object类里的方法wait和notify。 wait方法和sleep方法不一样,当使用obj对象的wait()方法后,该线程会释放锁,暂停执行转为等待状态。只有当其它线程调用了同一个对象的notify()方法后,该线程才会继续执行。

需要注意的是,在使用wait()和notify()时必须先获得obj的监视器,即它的锁。同时调用notify方法,只是从当前 obj对象的等待队列中随机选择一个等待线程将其唤醒,除非使用notifyAll方法,将等待队列里的所有线程全部唤醒。

正由于wait()/notify()这样的机制,使得多个线程能够依靠obj对象进行通信。

3). join方法与yield方法

当一个线程的输入依赖另一个线程的输出时,那么该线程需要等待其它线程执行完毕后,才继续执行。Thread类提供了join方法去将异步执行的线程变成同步执行。

join方法:

join()
join(long millis) //参数为毫秒
join(long millis,int nanoseconds) //第一参数为毫秒,第二个参数为纳秒

有参数的方法表示在一定的时间内等待,时间结束后线程会继续执行。注意该方法是在其它线程中调用该线程的join方法,即在main方法中调用newThread.join()方法时,主线程会等待子线程newThread执行完毕后才继续执行。

join方法的本质是调用线程的wait()方法在当前线程对象实例上。所以它是会释放对象锁的。源码为:

while(isAlive()){
    wait(0);
}

yield()方法:调用yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程。它跟sleep方法类似,同样不会释放锁。但是yield不能控制具体的交出CPU的时间,另外,yield方法只能让拥有相同优先级的线程有获取CPU执行时间的机会。

使用yield方法并不意味着放弃对CPU资源的争夺,让出CPU资源后,还会进行CPU资源的争夺。

2. volatile与java内存模型(JMM)

1). volatile关键字作用:

volatile只能保证多线程三大特性中的可见性和有序性。

可见性: 每一个线程都有一个自己的本地内存,对于共享变量,线程每次读取和写入的都是共享变量在本地内存的副本,然后在某个时间点将本地内存主内存的值进行同步。而当修改volatile修饰的变量时,会强制将对变量的修改同步到主内存。而其它线程在读取自己的本地内存中的值的时候,发现volatile修饰的变量值与主内存不一致,会将本地内存的值设为无效,然后从主内存中读取。

有序性: 在执行指令时,为了提高性能,处理器和编译器常常会对指令进行重排序,这种重排序一般只能保证单线程下执行结果不被改变。当变量被volatile修饰后,将会禁止重排序。

2). 实现原理

代码层面实现:通过内存屏障来实现的。所谓内存屏障,是指在某些指令中插入屏障指令。当虚拟机读取到这些屏障指令的时候会主动将本地内存的值刷新到内存,或直接从主内存中读取变量的值。通过屏障指令会禁止屏障前的操作命令和屏障后的操作指令进行重排序。

系统层面实现:在多处理器下,保证各个处理器的缓存是一致的,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态。

3). 应用场景

a. 多线程间状态标志;

b. 单例模式中的双重校验查锁的写法;

c. 定期观察成员变量状态的方法。

3. 线程组

为方便管理线程而定义的线程组,里面维护了线程的数组、是否为守护线程、优先级,线程组名等多个属性。

4. 守护线程

守护线程 指的是程序运行时在后台提供的一种通用服务的线程。 JDK 官方文档对守护线程的解释是:

当 JVM 中不存在任何一个正在运行的非守护线程时,则 JVM 进程即会退出。

常见的垃圾回收器即是守护线程。

5. 关键字synchronized

synchronized 是Java 中的关键字,是个同步锁。一般可修饰在代码块,方法,静态方法,类等位置。在不同位置修饰所起到的范围也会不同。

  • 修饰在代码块;被修改的代码块被称为同步语块,范围也是就是该代码块,作用的对象是调用这块代码的对象。
  • 修饰在普通方法;被修饰的方法被成为同步方法,作用的对象是调用该方法的对象;
  • 修饰在静态方法;范围是整个静态方法,作用对象是该类的所有对象;
  • 修饰在类;作用对象是该类的所有对象。