进程是代码在数据集合中的一次运行活动,是系统进行资源分配和调度的基本单位,线程是进程的一个执行路径,
一个进程中至少有一个线程,进程中的多个线程共享进程的资源。除了CPU资源之外,都是分配到进程的,CPU资源分配到线程。
创建线程的方式:继承Thread类,这样的方式是为了方便传参;实现Runnable类,只能使用主线程里面被声明为final的变量。这两种方式都不能返回结果,而FutureTask方式可以。
当一个线程调用一个共享变量的wait()方法时,该调用线程会被阻塞挂起,直到发生下面几件事情之一才返回:
(1)其他线程调用了该共享对象的notify()或者notifyAll()方法;(2)其他线程调用了该线程的interrupt()方法,该线程抛出InterruptedException异常返回。
当线程调用共享对象的wait()方法时,当前线程只会释放当前共享变量的锁,当前线程持有的其他共享变量的监视器锁并不会被释放。
线程在调用notify()之后,会唤醒调用wait()系列方法后被挂起的线程。
唤醒那个线程是随机的,在调用notify()方法之后,多个wait()方法,会争夺共享资源,那个线程获取共享变量,就执行那个线程。
notifyAll()方法会唤醒所有在该共享变量由于调用wait系列方法而被挂起的线程。
join()等待子线程运行完毕,yield让出cpu,调用sleep方法,改线程会暂时让出指定时间的执行权(这段时间内,该线程不参与cpu的调度)。
当调用sleep方法时,线程会被阻塞挂起指定的时间,在这期间线程调度器不会调用改线程;调用yield方法是,线程只是让出自己剩余的时间片,并没有被阻塞挂起,而是处于就需状态,线程调度器下一次调度就有可能调度到当前线程执行。
Java中的线程中断是一种线程间的协作模式,通过设置线程的中断标志并不能直接终止该线程的方法,而是被中断的线程根据中断状态自行处理。
void interrupt()方法:中断线程(设置标记位)。线程A在的运行过程中,线程B调用这个方法,线程A会被设置上标记,如果线程A处于阻塞被挂起时,线程B调用改方法,线程A会抛出InterruptedException异常。
boolean isInterrupted()方法:检测当前线程是否被中断(获取中断位置)。如果是返回true,否则返回false。
boolean interrupted()方法:检测当前线程是否被中断,如果是返回true,否则返回false。如果当前线程被中断,清除中断标志。
产生死锁必须具备的四个条件:
1.互斥条件:线程对已经获取的资源进行排他性使用,即该资源同时只由一个线程占用。
2.请求并持由条件:指一个线程已经柴油至少一个资源,但又提出新的资源请求,而新资源已被其他线程占有,所以当前线会被程阻塞,但阻塞的同时并不释放自己已经获取的资源。
3.不可剥夺条件:线程获取到的资源在自己使用完之前不能被其他线程抢占,只有在自己使用完毕后才有自己释放该资源。
4.环路等待条件:在发生自锁时,必然寻在一个线程--资源的环形链。
如何避免死锁:
目前只有请求并持有和环路等待条件是可以被破坏的:使用资源和申请的有序性原则。
Java线程分为:daemon线程(守护线程)和user线程(用户线程)。
守护线程和用户线程的区别:当最后一个非守护线程结束时,JVM会正常退出,不管当前是否又守护进程,即守护进程是否结束并不影响JVM的退出。
子线程的生命周期并不受父线程的影响。
如果希望主线程结束之后JVM进程马上结束,在创建线程时可以将其设置为守护进程,如果希望在主线程结束后子线程继续工作,等子线程结束后再让JVM进程结束,那么将子线程设置为用户线程。
线程上下文切换时机有:当前线程的CPU时间片使用完处于就绪状态时,当前线程被其他线程中断时。
synchronized是内置锁,内置锁是排他锁,就是当一个线程获取这个锁后,其他线程必须等待该线程释放后才能获取该锁。synchronized的使用导致上下文切换。
进入synchronized块,使用的变量值都是从内存中直接获取,退出synchronized块的内存语义是把在synchronized块内存中修改的共享变量刷新到祝内存中。
synchronized经常被用来实现原子性操作。synchronized关键字会引起线程上下文切换并带来线程调度开销。
violate 是从主内存中直接读取数据。
violate与synchronized的区别:当线程写入了violate变量值时就等价于线程退出synchronized同步块,读取violate变量值时就相当于进入同步块。
synchronized是排他锁,将其他线程阻塞,保存线程上下文切换和线程重新调度的开销;violate是非阻塞算法,不会造成线程上下文切换的开销。
ABA问题的产生是因为变量的状态值产生了环形转换。JDK中的AtomicStampedReference类给每个变量的状态值都配备了一个时间戳,从而避免了ABA问题的产生。
violate只能保证共享便来给你的可见行,不能解决读-改-写等的原子性问题。
CAS是JDK提供的非阻塞原子性操作,通过硬件保证了比较-更新操作的原子性。
如果想要使用Unsafe实例,就需要使用反射来获取。
重排序在多线程下会导致非预期的程序执行结果,而使用volatile修饰的变量避免重排序和内存可见性问题。
写volatile变量时,可以确保volatile写之前的操作不会被编译器重排序到volatile写之后。
读volatile变量时,可以确保volatile读之后的操作不会被编译器重排序到volatile读之前。
cache内部是按照行存储的,每一行为cache行。cache行是cache与主内存进行数据交换的单位。
当CPU访问某个变量时,先看看CPU cache是否有该变量,如果有,直接取,没有的话,去主内存中获取,然后将变量所在的内存
区域的一个cache行大小的内存复制到cache中。由于存放cache行的内存块不是单个变量,所有可能会将多个变量放入到一个cache行中。
当多个线程同时修改一个缓存行里面的多个变量时,由于同时只能有一个线程操作缓存行,所以相比将每个变量当如到一个缓存行,
性能会有下降,这就是伪共享。
伪共享的产生是因为多个变量被放入一个缓存行中,并且多个线程同时去写入缓存行中不同的变量。
在JDK8之前,一般是通过字节填充的方式避免伪共享的问题,也就是创建一个变量时使用填充字段填充该变量所在的缓存行,这样避免了多个变量放在同一个缓存行中。
JDK8提供了一个sun.misc.Contented注解,用来解决伪共享。
悲观锁:在数据处理之前对数据进行加锁,并在整个处理过程中,使数据处于锁定状态。如果获取锁失败,说明数据正在被其他线程修改,当前线程等待或者抛出异常。
如果获取锁成功,则对记录进行操作,然后提交事务后释放排他锁。
乐观锁在访问记录前不会加排他锁,而是在进行数据提交更新时,才会正式对数据冲突是否进行检测。
乐观锁并不会使用数据库提供的锁机制,一般在表中添加version字段或者使用业务状态来实现。乐观锁知道提交时才锁定,所以不会产生任何死锁。
根据线程获取锁的抢占机制,锁可以认为公平锁和非公平锁,公平锁表示线程获取锁的顺序是按照线程请求锁的时间早晚来决定的,也就是最早请求锁的线程将最早获取到锁(开销大)。
非公平锁:先来不一定先得。
根据锁只能被单个线程持有还是多个线程公共持有,锁可以分为独占锁(悲观锁)和共享锁。
独占锁保证任何时候都有一个线程能得到锁,ReentrantLock就是以独占锁方式实现的。
共享锁可以同时有多个线程持有,允许一个资源可以被多线程同时进行读操作。
独占锁是一种悲观锁,由于每次访问资源都先加上互斥锁,这限制了并发性。
共享锁是一种乐观锁,它放宽了加锁的条件,允许多个线程同时进行读操作。
一个线程再次获取它自己已经获取的锁,如果不发生阻塞,说明这个锁是可重入的,也就是该线程获取了该锁,那么可以无限次地进入被该锁锁住的代码。
synchronized内部锁是可重入锁。可重入锁有一个计数器,如果是当前的对象,计数器加1,如果放弃锁,计数器减1。
自旋锁:当前线程在获取锁时,如果发现锁已经被其他线程占有,它不马上阻塞自己,在不放弃CPU使用权的情况下,多次尝试获取,如果超过指定次数,还没有获取锁,线程才会阻塞挂起。
自旋锁使用CPU时间换取线程阻塞与调度的开销,但是很有可能这些CPU时间白白浪费。
网友评论