线程基础
进程和线程的关系
线程是进程中的一个实体,线程本身不会独立存在。进程是代码在数据集合上的一次运动活动,是系统进行资源分配和调度的基本单元,线程则是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的资源。
创建的线程的方式
方式一
public class CusThread extends Thread{
@Override
public void run() {
super.run();
}
}
方式二
public class CusRunable implements Runnable{
@Override
public void run() {
}
}
方式三
public class CallerTask implements Callable<String>{
@Override
public String call() throws Exception {
return "Hello Caller";
}
}
main(){
FutureTask<String> futureTask=new FutureTask<>(new CallerTask());
new Thread(futureTask).start();
try {
String result=futureTask.get();
} catch (ExecutionException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
第一种通过类继承的方式但是java只支持单继承就不能继承其它类,任务与代码没有分离,多个线程执行一样的任务时需要多份任务代码。
第二种通过Runable接口的方式没有第一种的限制更灵活。
第三种有返回值,调用get方法内部会通过轮询的方式拿结果。
线程相关行为
wait()
当一个线程调用一个共享变量的wait()方法时,该调用的线程会被阻塞刮起,直到发生下面几个事情之一才返回(1)其他线程调用了该共享对象的notify()或notifyAll()方法(2)其他线程调用了该线程的interrupt()方法,该线程抛出InterruptedException异常返回.
另外需要注意的是,如果调用wait()方法的线程没有事先获取该对象的监视器锁,则调用wait()方法时调用的线程会抛出IllegalMonitorStateException异常
wait(long timeout)
该方法相比wait()方法多了一个超时参数,它的不同之处在于,如果一个线程调用共享对象的该方法挂起后,没有在指定的timeout ms时间内被其他线程调用该共享变量的notity()或者notityAll()方法唤醒,那么该函数还是会因此超时而返回。
notity()
一个线程调用共享对象的notity()方法后,会唤醒一个在该共享变量上调用wait系列方法后被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程时随机的。
notityAll()
不同于在共享变量上调用notity()函数会唤醒被阻塞到该笔共享变量上的一个线程,notityAll()方法则会唤醒所有在该共享变量上由于调用wait系列方法而被挂起的线程。
join()
如果多线程情况下让线程有序执行可以通过join()方法,其中一个线程调用join方法后被阻塞,等待线程执行结果返回有序去执行下一个线程调用join()方法。
sleep()
Thread类中有一个静态的sleep方法,当一个执行中的线程调用了Thread的sleep方法后,调用线程会暂时让出指定时间的执行权,也就是在这个期间不参与CPU的调度,但是该线程所拥有的监视器资源,比如锁还是持有不让出的。指定的睡眠时间到了后该函数会正常返回,线程就处于就绪状态,然后参与CPU调度,获取到cpu资源后就可以继续运行了。
yield()
Thread类中有一个静态的yield方法,当一个线程调用yield方法,实际就是在暗示线程调度器当前线程请求让出自己的CPU使用。
interrupted()
java中的线程中断时一种线程间的协作模式,通过设置线程的中断标志并不能直接终止该线程的执行,而是被中断的线程根据中断状态自行处理。
线程上下文切换
在多线程编程中,线程个数一般都大于CPU个数,而每个CPU同一时刻只能被一个线程使用,为了让用户感觉多个线程时在同时执行的,CPU资源的分配采用了时间片轮转的策略,也就是给每个线程分配一个时间片,线程在时间片内占用CPU执行任务。当前线程使用完时间片后,就会处于就绪状态并让出CPU让其他线程占有,这就是上下文切换,从当前线程的上下文切换到了其他线程。
死锁
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的相处,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去。
死锁的四个条件
互斥条件:指线程对已经获取到的资源进行排它性使用,既该资源同时只由一个线程占用。如果此时还有其他线程请求获取该资源,则请求者只能等待,直至占有资源的线程释放该资源。
请求并持有条件:指一个线程已经持有了至少一个资源,但又提出了新的资源请求,而新资源已被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己已经获取的资源。
不可剥夺条件:指线程获取到的资源在自己使用完之前不能被其他线程抢占,只有在自己使用完毕后才由自己释放该资源。
环路等待条件:指在发生死锁时,必然存在一个线程-资源的环形链,既线程集合{T0,T1,T2,...,Tn}中的TO在在等待一个T1占用的资源,T1正在等待T2占用的资源,...Tn正在等待被T0占用的资源。
守护线程与用户线程
Java中的线程分为两类,分别为daemon线程(守护线程)和user线程(用户线程).在JVM启动时会调用main函数,main函数所在的线程就是一个用户线程,其实在JVM内部同时还启动了好多守护线程,比如垃圾回收线程。
守护线程和用户线程的区别
最后一个非守护线程结束时,JVM会正常退出,而不管当前是否有守护线程,也就是说守护线程是否结束并不影响JVM的退出。
并发并行概念
并发:是指同一个时间段内多个任务同时都在执行。
并行:单位时间多个任务同时执行。
共享资源:资源被多个线程所持有或者说多个线程都可以去访问资源。
线程安全问题:线程安全问题是指多个线程同时写一个共享资源并且没有任何同步措施时,导致脏数据或其他不可预见的结果问题。
synchronized:Java提供的一种原子内部锁,Java中的每个对象都可以把它当作一个同步锁来使用,这些Java内置的使用者看不到的锁被称为内部锁,也叫作监视器锁。 线程的执行代码在进入synchronized代码块前会自动获取内部锁,这时候其他线程访问该同步代码块时会被阻塞挂起。
syschronized关键字会引起线程上下文切换并带来线程调度开销
为什么使用synchronized会导致线程上下文切换
由于Java中的线程是与操作系统的原生线程一一对应的,所以当阻塞一个线程时,需要从用户态切换到内核态执行阻塞操作,这是很耗时的操作,而synchronized的使用就会导致上下文切换。
volatile:java提供一种弱形式的同步,该关键字可以确保对一个变量的更新对其他线程马上可见。
使用volatile的场景
- 写入变量值不依赖变量的当前值时。因为如果依赖当前值,将时获取一计算一写入三步操作,这三步操作不是原子性的,而volatile不保证原子性。
- 读写变量值时没有加锁。因为加锁本身已经保证了内存可见性,这时候不需要吧变量声明为volatile的。
CAS(Compare And Swap)
**cas:**JDK提供的非阻塞原子操作,它通过硬件保证了比较和更新操作的原子性。
锁
悲观锁:悲观锁指对数据被外界修改持保守态度,认为数据很容易就会被其他线程修改,所以在数据被处理前先对数据进行加速,并在整个数据处理过程中,使数据处于锁定状态。
乐观锁:乐观锁是相对悲观锁来说的,它认为数据在一般情况下不会造成冲突,所以在访问记录前不会加排它锁,而是在进行数据提交更新时,才会正式对数据冲突与否进行检测。
公平锁:表示线程获取锁的顺序是按照线程请求锁的时间早晚来决定的。(公平锁会带来性能开销)
非公平锁:运行时获取锁,也就是先来不一定先得。
独占锁:独占锁保证任何时候都只有一个线程能得到锁(举例ReentrantLock独占锁),独占锁属于悲观锁,每次访问资源都加上互斥锁,限制了并发性
共享锁:共享锁可以同时由多个线程持有(举例ReadWriteLock读写锁 共享锁),共享锁是一种乐观锁。
可重入锁:线程获取一个被其他线程持有的独占锁不被阻塞就是可重入锁(synchronized就是可重入锁),可重入锁的原理是在锁内部维护一个线程标示。
自旋锁:当线程在获取锁时,如果发现锁已经被其他线程占有,它不会马上阻塞自己,在不放弃CPU使用权的事情下,多次尝试获取(默认次数是10)很有可能在后面几次尝试中其他线程已经释放了锁。如果尝试指定的次数后仍没有获取到锁则当前线程才会被阻塞挂起。