简介
进程是分配资源的基本单位,而线程是系统调度的基本单位,一个进程可以包含多个线程,这些线程共享进程的资源。
并发
并行与并发
- 并行:多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时。
- 并发:通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。并发往往在场景中有公用的资源,那么针对这个公用的资源往往产生瓶颈,我们会用TPS或者QPS来反应这个系统的处理能力。
并发的三个概念
1. 原子性
即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。可以通过加锁来实现。
2. 可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。可以通过volatile来实现。
3. 有序性
即程序执行的顺序按照代码的先后顺序执行。
一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。在单线程中指令重排序并不会影响最后的结果,但是在多线程并发的情况下,可能会导致结果和串行的不一致。可以通过volatile来保证有序性,因为volatile加入了内存屏障防止指令重排序。
另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。
另外,Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。
线程的生命周期
新建和就绪
程序使用new会新建一个线程,new出的对象跟普通对象一样,JVM会为其分配内存,初始化成员变量等,此时线程并没有运行,而是就是新建状态。
当线程对象调用start后,线程将进入就绪状态。JVM会为其创建函数调度栈和计数器,但此时线程依然没有运行,而是等待获取CPU执行片。
运行和阻塞状态
当就绪状态的线程获取了CPU执行片的之后,就进入运行状态,但是在执行过程中,可能会因为以下原因使线程进入阻塞状态,
① 线程调用sleep()方法主动放弃所占用的处理器资源
② 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞
③ 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。关于同步监视器的知识、后面将有深入的介绍
④ 线程在等待某个通知(notify)
⑤ 程序调用了线程的suspend()方法将该线程挂起。但这个方法容易导致死锁,所以应该尽量避免使用该方法
线程从运行进入阻塞状态之后,接着只能继续阻塞或者再次进入就绪状态,下面情况会使线程由阻塞状态重新进入就绪状态,
① 调用sleep()方法的线程经过了指定时间。
② 线程调用的阻塞式IO方法已经返回。
③ 线程成功地获得了试图取得的同步监视器。
④ 线程正在等待某个通知时,其他线程发出了个通知。
⑤ 处于挂起状态的线程被调甩了resume()恢复方法。
死亡状态
① run()或call()方法执行完成,线程正常结束。
② 线程抛出一个未捕获的Exception或Error。
③ 直接调用该线程stop()方法来结束该线程——该方法容易导致死锁,通常不推荐使用
wait与sleep的差别
1. 对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的。
2. sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。
在调用sleep()方法的过程中,线程不会释放对象锁。
而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备
监视器monitor
Java中的每个对象都有一个监视器,来监测并发代码的重入。为了实现监视器的互斥功能,每个对象(Object和class)都关联着一个锁(有时也叫“互斥量”),这个锁在操作系统书籍中称为“信号量”,互斥(“mutex”)是一个二进制的信号量 [每个对象同时只能被一个线程获取锁]。
JAVA使用wait()和notify()和notifyAll()方法实现线程的挂起和唤醒。此可以用来实现消费者和生产者模式。
public class Product_Consumer {
public static void main(String[] args){
Storage storage = new Storage();
for(int i=0;i<20;i++){
Thread producter = new Thread(new Runnable() {
@Override
public void run() {
storage.product();
}
});
Thread consumer = new Thread(new Runnable() {
@Override
public void run() {
storage.consume();
}
});
producter.start();
consumer.start();
}
}
}
class Storage{
public static final int MAX_STORAGE = 10;
public static int nowStorage = 0;
public synchronized void product(){
if(nowStorage>=MAX_STORAGE){
try {
System.out.println("库存已满,此时库存为"+nowStorage);
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
return;
}
nowStorage++;
System.out.println(Thread.currentThread().getName()+"生产了一件产品,目前库存为"+nowStorage);
notifyAll();
}
public synchronized void consume(){
if (nowStorage<=0){
try {
System.out.println("库存为空,此时库存为"+nowStorage);
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
return;
}
nowStorage--;
System.out.println(Thread.currentThread().getName()+"消费了一件产品,目前库存为"+nowStorage);
notifyAll();
}
}
多线程创建
public class MyThread {
public static void main(String[] args){
//方法1:继承Thread
for(int i=0;i<10;i++){
ThreadTest threadTest = new ThreadTest();
threadTest.start();
}
//方法2:实现Runnable接口
for(int i=0;i<10;i++){
RunnableTest runnableTest = new RunnableTest();
Thread thread = new Thread(runnableTest);
thread.start();
}
//方法3:实现Callable接口
for(int i=0;i<10;i++){
CallableTest callableTest = new CallableTest();
FutureTask<String> futureTask = new FutureTask<String>(callableTest);
Thread thread = new Thread(futureTask);
thread.start();
try {
System.out.println(futureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
}
class ThreadTest extends Thread{
@Override
public void run(){
System.out.println("EXTENDS THREAD "+this.getName());
}
}
class RunnableTest implements Runnable{
@Override
public void run(){
System.out.println("IMPLEMENTS RUNNABLE "+Thread.currentThread().getName());
}
}
class CallableTest implements Callable{
@Override
public String call() throws Exception {
return "IMPLEMENTS CALLABLE "+Thread.currentThread().getName();
}
}
在java中多线程的创建有三种方式,分别为继承Thread,实现Runnable和Callable接口。
Callable的区别: Callable可以有返回值以及可以处理异常,Thread和Runnable不可以。
实现Runnable接口比继承Thread类所具有的优势:
1):适合多个相同的程序代码的线程去处理同一个资源
2):可以避免java中的单继承的限制
3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
线程间的同步
synchronized 和 ReentrantLock
public class ThreadLock {
public static int i = 10;
public static final ReentrantLock reentrantLock = new ReentrantLock();
public synchronized void synTest(){
i++;
}
public void reenTrantTest(){
try{
reentrantLock.lock();
i--;
}finally {
reentrantLock.unlock();
}
}
}
区别:
1.首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
2.synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
3.synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
4.用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
5.synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)
6.Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
ThreadLocal
https://www.cnblogs.com/dolphin0520/p/3920407.html
public class ThreadLocalTest {
public static void main(String[] args){
System.out.println(MyThreadLocal.threadLocal.get());
MyThreadLocal.threadLocal.set("hello world");
System.out.println(MyThreadLocal.threadLocal.get());
MyThreadLocal.threadLocal.remove();
}
}
class MyThreadLocal{
public static ThreadLocal<String> threadLocal = new ThreadLocal<String>(){
@Override
protected String initialValue(){
return "x";
}
};
}
volatile
https://www.cnblogs.com/dolphin0520/p/3920373.html
因为数据是存储在主存中,CPU的处理速度远高于从主存中存取数据的速度,如果直接从主存中获取数据会非常的影响性能,因此CPU中会有一个高速缓存。
当多个CPU对同一数据进行处理时就会存在数据不一致的现象。
解决这种不一致的情况有以下两种方法:
1. 在总线上加锁,例如synchronized。
2. 缓存一致协议,即依靠volatile关键字来实现。
而volatile约定的内容有以下几点:
1. 当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
2. volatile修饰的数据在进行写操作后要立即写入主存。
3. 禁止指令重排序。
volatile的原理和实现机制
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
volatile常见使用场景:单例模式双检锁,线程池状态
原子类
https://www.cnblogs.com/chengxiao/p/6789109.html
容器类
https://blog.csdn.net/sjjsh2/article/details/53286001
Iterator与for的区别:
https://blog.csdn.net/hufeng19940810/article/details/77933768
1. Iterator适合顺序访问(linkedlist),for适合随机访问(arraylist)
2. foreach的内部是依靠Iterator实现的
3. 如果在for中调用remove进行删除,会报越界错误,但是如果使用Iterator则不会,因为它的remove()方法不仅会删除元素,还会维护一个标志,用来记录目前是不是可删除状态,例如,你不能连续两次调用它的remove()方法,调用之前至少有一次next()方法的调用。
线程池
https://www.cnblogs.com/dolphin0520/p/3932921.html
java.uitl.concurrent.ThreadPoolExecutor类是线程池中最核心的一个类
其构造函数有以下几个参数:
corePoolSize:核心池的大小,线程池刚创建时里面的线程数是为0的,当有任务需要处理时才开始创建线程,当线程数达到corePoolSize的时候,再有新任务需要处理时,则不再创建新的线程,而是将任务放在缓存队列中。
maximumPoolSize:线程池最大线程数。是线程池的一种补救措施,当任务量突然增大时,可以在corePoolSize的基础上增加线程。
keepAliveTime:当线程数超过corePoolSize的时候,keepAliveTime才起作用,当一个线程没有任务执行时间达到keepAliveTime时则进行销毁,直到线程数低于corePoolSize。
unit:keepAliveTime的单位。
workQueue:阻塞队列,一般使用LinkedBlockingQueue和Synchronous比较多。
threadFactory:线程工厂,用于创建线程的。
handler:拒绝处理任务时的策略有以下四个策略。
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。 ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程) ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
核心方法:
execute() : 通过此方法向线程池提供任务,交由线程池处理。
submit() : 这个方法也是用来向线程池提交任务的,但是它和execute()方法不同,它能够返回任务执行的结果,去看submit()方法的实现,会发现它实际上还是调用的execute()方法,只不过它利用了Future来获取任务执行结果.
shutdown()和shutdownNow()是用来关闭线程池的。
还有很多其他的方法:
比如:getQueue() 、getPoolSize() 、getActiveCount()、getCompletedTaskCount()等获取与线程池相关属性的方法
线程池的状态
public class ThreadPool {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 10, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3));
for (int i = 0; i < 20; i++) {
MyTask myTask = new MyTask(i);
executor.execute(myTask);
System.out.println("线程池中总线程数:"+executor.getPoolSize());
System.out.println("等待队列中任务数:"+executor.getQueue().size());
System.out.println("已处理的任务数:"+executor.getCompletedTaskCount());
}
executor.shutdown();
}
}
class MyTask implements Runnable {
private int taskName;
public MyTask(int taskName) {
this.taskName = taskName;
}
@Override
public void run() {
System.out.println("正在执行task" + taskName);
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("task" + taskName + "执行完毕");
}
}
死锁
当有两个或更多的线程在等待对方释放锁并无限期地卡住时,这种情况就称为死锁。在java中出现嵌套的同步块,或者在同步块中企图锁定不同的对象时就容易发生死锁。
class DeadLockMthod{
private String a = "xxx";
private String b = "ccc";
public void method1(){
synchronized (a){
System.out.println("method1 get a");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (b){
System.out.println("method1 get b");
}
}
}
public void method2(){
synchronized (b){
System.out.println("method2 get b");
synchronized (a){
System.out.println("method2 get a");
}
}
}
}
public class DeadLock{
public static void main(String[] args) {
DeadLockMthod deadLockMthod = new DeadLockMthod();
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
deadLockMthod.method1();
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
deadLockMthod.method2();
}
});
threadA.start();
threadB.start();
}
}
产生死锁的四个必要条件:
(1) 互斥条件:一个资源每次只能被一个进程使用。
(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
避免死锁:
死锁的产生并不是因为多线程,而是因为请求锁的方式或者顺序不正确,则只要对锁进行一个合理的规划,实现一个有序的访问则可以避免死锁。
class DeadLockMthod{
private String a = "xxx";
private String b = "ccc";
public void method1(){
synchronized (b){
System.out.println("method1 get b");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (a){
System.out.println("method1 get a");
}
}
}
public void method2(){
synchronized (b){
System.out.println("method2 get b");
synchronized (a){
System.out.println("method2 get a");
}
}
}
}