java进阶篇03、并发编程基础概念

164 阅读11分钟

一、进程与线程基础概念

1.进程与线程

进程是cpu分配资源的最小单位,资源包括cpu、内存、磁盘及io等;

线程是cpu调度的最小单位;

进程代表应用程序,应用程序是静态概念,进程是应用程序动态运行的状态表示;

一般一个进程会包含多个线程,多个线程同时共享进程分配到的资源;

每个进程具体的工作任务交给具体的线程进行处理;

2.CPU核心数与线程数的关系

现代cpu一般使用多核心和超线程技术,主流为4核8线程、6核12线程、8核16线程等;

3.cpu的时间片轮转机制

cpu在处理程序的时候,会分成多个时间片,会给每个程序分配一定的时间片,时间片到期后会继续分配给其他程序;

4.并行和并发

并行是指多个线程同时执行,例如有两台咖啡机,两队人员同时打咖啡;cpu的多核心或者多线程同步运行就可以表示为并行;

并发是指一定时间内多道程序交替运行,例如有一台咖啡机,两队人员交替打咖啡;cpu的单核心或者单个线程通过时间片轮换机制执行程序就可理解为并发;

5.高并发编程的意义、好处和注意事项

现代cpu都是多核心超线程的,如果程序支持多线程,可以大大提高程序的运行效率;如果程序仅为单线程执行的,则可能会大大浪费cpu的多线程;另外编写多线程程序时,要注意同步问题,因为一个进程的多个线程共享此进程的资源数据;

java的程序天生就是多线程的,如果你开一个main方法然后运行,打印出正在执行的线程,会发现同时运行多个线程,还会有守护进程等等;

6.新启动线程的方法

严格来说有两类新启线程的方法;第一类是通过继承Thread类;第二类是通过实现Runnable接口或者Callable接口;

Thread是对操作系统底层线程的抽象,而Runnable和Callable是对业务逻辑(任务)的抽象;

Runnable和Callable在多线程方面定义任务相比Thread更加灵活;

Runnable不能直接运行,还是得借助于Thread运行;

7.Thread的start方法和run方法区别

start方法是真正的开启一个线程去执行run方法,而run方法只是Thread类内的一个普通的方法,我们可以直接调用run方法,但此时并不是在新开的线程中运行的,还是在当前线程中运行的;

8.怎么样让java里的线程安全停止工作

现在stop、destroy和suspend等方法都已经废弃了,因为这些方法对线程进行操作太过暴力,直接停止销毁线程的操作是不安全的,因为线程可能会持有锁或者是资源,不能正确的进行释放;

正确的停止线程的方法应该是通过interrupt方法,给线程发送一个中断信号,此时isInterrupted和静态的interrupted标志位都被置为true,我们应该在自己的线程中通过判断标志位处理中断信号;

isInterrupted和interrupted的区别,静态的interrupted方法在检查完标志位后会把标志位重新置为false,而isInterrupted方法并不会对标志位重新置位;

9.线程状态和线程常用方法

线程分为新建、可运行、运行、阻塞和终止五大状态;

new出一个Thread时线程处于新建状态,当执行Thread的start方法时,线程处于可运行状态,当分配到cpu时间片后才真正进入了运行状态,当调用了sleep或wait方法后线程进入阻塞状态,当run方法运行结束或者调用stop方法或者将一个线程设为守护线程时(setDeamon方法,例如垃圾回收线程就是守护线程),当被守护的线程终止时,守护线程也会立即停止,守护线程run方法中的finally块可能都不一定执行就被终止了。

join()方法,让调用join方法的线程优先执行,join线程执行完成之后当前线程才能继续执行,将两个交替执行的线程顺序执行;

yield()方法,让出cpu资源,从运行状态重新返回可运行状态,等待下一次cpu的调度,调用此方法不代表线程不运行了,线程可能很快又重新被cpu调度了;

sleep()方法,线程进入阻塞状态,当sleep时间到了之后重新变成可运行状态;

setPriority() 设置线程的优先级,1-10级,并不可靠,操作系统的优先级只有三级;

10.synchronized关键字

synchronized关键字可用在方法上或者同步代码块上;

synchronized分为对象锁和类锁,其实synchronized锁的是对象,当用在静态方法或者在代码块中synchronized对静态对象加锁,此时称为类锁,但是其实也是锁的类对象,只不过一个类的class对象在虚拟机中只有一份,所以称之为类锁;

在线程中对象锁和类锁是可以同时运行的,因为他们synchronized锁的对象不同,不存在竞争关系

如果一个线程处于了阻塞状态(如线程调用了thread.sleep、thread.join、 thread.wait 等),则在线程在检查中断标示时如果发现中断标示为 true,则会在这些阻塞方法调用处抛出 InterruptedException 异常,并且在抛出异常后会立即将线程的中断标示位清除,即重新设置为 false;

11.volatile关键字,最轻量的同步机制

volatile保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的;

volatile 不能保证数据在多个线程下同时写时的线程安全;

volatile适合那种一个线程写,多个线程读的情况;

volatile关键字保证可见性和有序性(禁止指令重排),但不保证原子性;

使用volatile应具备以下两个条件:

  1. 对变量的写操作不会依赖当前值;
  2. 该变量没有包含在具有其他变量的不变式中;

二、ThreadLocal简介

1.ThreadLocal与synchronized对比

ThreadLocal与synchronized都用于解决多线程并发问题,但synchronized是利用锁的机制保证同一时刻每个方法或代码块只有一个线程访问,而ThreadLocal为每个线程都提供了变量的副本,使得每个线程在某一时刻访问到的并非同一对象,这样就隔离了多个线程对数据的数据共享。

2.ThreadLocal的基本使用

get方法用于获取线程保存的变量的值;

set方法用于设置线程对应的变量的值;

remove方法用于移除线程中保存的对应变量的值,当线程执行完毕时会自动释放value的引用,但通过调用remove可以加快内存回收,防止内存泄漏;

initialValue返回该线程对应的变量的值,仅在set或者get时调用一次,初始方法返回null;

3.实现解析

首先明确几个类,Entry类是TheadLocalMap类的静态内部类,TheadLocalMap类是ThreadLocal类的静态内部类,Thread类中持有TheadLocalMap类的对象,我们保存线程隔离的对象时是在Entry中保存的,Entry非常类似map,其中key保存的就是ThreadLocal的弱引用,value保存的就是线程隔离的对象。

TheadLocalMap类中维持着Entry类的数组,用于保存多个ThreadLocal和线程隔离对象。

set方法:首先获取当前线程,然后获取当前线程的TheadLocalMap对象:如果此map对象不为空,通过map.set方法设置值,而map.set内部又是通过新建了一个Entry对象保存了ThreadLocal对象和线程隔离对象,然后维护一下TheadLocalMap中的Entry数组;如果此map对象为空,则通过new新建一个map对象,内部也是新建了一个Entry对象;

get方法:同样是首先获取当前线程,然后获取当前线程的TheadLocalMap对象:如果此map对象不为空,通过map.getEntry获得Entry对象,然后通过entry.value获得线程隔离的对象并返回;如果此map对象为空,则调用setInitialValue方法,在此方法内部又会调用initialValue方法返回设置的值,然后新建map,将ThreadLocal和通过initialValue方法返回的值保存到Entry中去;

remove方法:首先获取当前线程中保存的TheadLocalMap对象,如果此map不为空,则调用map的remove方法,在此方法中会将对应的entry对象清除掉

4.ThreadLocal内存泄漏

因为在Entry中key为ThreadLocal的弱引用,当外部没有对此ThreadLocal的强引用存在后,此ThreadLocal就会被垃圾回收,此时Entry中保存的value没法被访问到了,按理说应该被回收掉,但是Thread内部持有TheadLocalMap对象,而TheadLocalMap对象又会持有此Entry,Entry中保存着value,在当前线程没有结束之前一直都持有此Entry的引用,因此没法释放,造成内存泄漏。

最好的解决办法就是在使用完threadlocal隔离的对象后,手动调用一下threadlocal的remove方法,remove方法会将entry和entry中的value都置为null,此时value对象没有引用指向了,就可以被垃圾回收掉了。

虽然get和set方法也会检查ThreadLocal为null的entry,然后将value置为null,但是这个操作是不及时的,所以还是有可能造成内存泄漏。

三、线程间的协作

1.等待通知机制

notify方法:通知一个在该对象上等待的线程,使其从wait方法处返回继续执行,前提是该线程通过竞争获取到了对象上的锁;

notifyAll方法:通知所有在该对象上等待的线程,那个线程竞争获取到了锁就从wait处继续执行

wait方法:当前线程进入waiting等待状态,调用wait后会释放锁;

wait(long):超时等待,同样进入waiting等待状态,超时时间到了仍然没有被唤醒则会超时返回;

2.等待通知的标准范式写法

等待方:首先获取对象的锁,如果条件不满足调用wait进入等待状态,被通知后仍要通过while循环检查条件,不满足继续调用wait等待,否则执行对应逻辑;

通知方:首先获取对象的锁,然后通过逻辑去改变条件,最后在同步方法或者同步代码块中最后一句调用notify或者notifyAll,因为这两个方法不会释放锁;

在调用wait或者notify之前,线程必须要获得该对象的对象级别锁,所以只能在同步方法或者同步代码块中调用wait和notify方法;

3.notify和notifyAll应该用谁

应该尽量使用notifyAll,因为notify只能唤醒当前对象等待的一个线程,而这个线程可能并不是我们需要的线程,然后此线程检查条件仍然不满足,重新进入等待状态,此时就会进入死锁的状态;只有在目标非常明确,确定调用notify不会出现差错时才会掉用notify,例如只有一个检查条件时,此时调用了notify表明这一个条件变化了,然后等待的线程通过while判断变化后的条件就可以进行正确的操作了。

4.调用yield、sleep、wait和notify方法对锁有何影响

yield和sleep方法都不会释放锁;

wait方法会释放当前线程的锁,而当被重新唤醒后,会重新竞争获取锁,如果获取不到继续等待进行下一次竞争获取;

notify方法也不会释放锁,所以最好把notify或者notifyAll方法放在同步方法或者同步代码块的最后一行,这样运行完notify或者notifyAll方法后也就同时把锁释放了,让处于wait状态的线程去竞争锁;