java多线程整理(一)

零、基础概念:
1、同步VS异步:同步和异步通常用来形容一次方法调用。同步方法调用一开始,调用者必须等待被调用的方法结束后,调用者后面的代码才能执行。而异步调用,指的是,调用者不用管被调用方法是否完成,都会继续执行后面的代码,当被调用的方法完成后会通知调用者。
2、并发与并行:并发和并行是十分容易混淆的概念。并发指的是多个任务交替进行,而并行则是指真正意义上的“同时进行”。实际上,如果系统内只有一个CPU,而使用多线程时,那么真实系统环境下不能并行,只能通过切换时间片的方式交替进行,而成为并发执行任务。真正的并行也只能出现在拥有多个CPU的系统中。
3、阻塞和非阻塞:阻塞和非阻塞通常用来形容多线程间的相互影响,比如一个线程占有了临界区资源,那么其他线程需要这个资源就必须进行等待该资源的释放,会导致等待的线程挂起,这种情况就是阻塞,而非阻塞就恰好相反,它强调没有一个线程可以阻塞其他线程,所有的线程都会尝试地往前运行。阻塞的代表就是synchronized锁,CAS算法就是非阻塞。
4、临界区:临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每个线程使用时,一旦临界区资源被一个线程占有,那么其他线程必须等待。

一、多线程的实际应用:
实际项目中对于一些主线程无需等待,但是又需要做一些其他操作并且此操作又不影响主线程执行结果的时候,条件允许情况下我们都可以用多线程来实现比如某个请求返回结果的多次处理,直接返回请求,并且对返回结果进行多种处理,这时候完全可以开异步线程去处理其他操作;还有就是需要将一个主线程拆分成多个子线程去处理,主线程阻塞住等待异步线程依次返回请求后汇总然后返回请求结果,尤其是涉及到多个分支请求时候我们也不会去用主线程一个个请求;还有一些其他的比如mysql数据进比较猛,我们改怎么解决?使用缓冲队列,这个大数据中的sparksql+kafka组合原理类似,采用批量拉取,避免一条条操作,对mysql压力过大,简单说下实现,数据先入arrayblockingqueue,然后使用ThreadPoolConfig中维护的线程池在调用方法上@Async来实现并发拉取队列消息,消费方法里面使用take()自动阻塞,然后来消息后触发队列的drainTo(list)方法来进行一次性拉取,批量插入,最后消息的监听可以采用实现CommandLineRunner中的run方法启动后调用队列的消费方法,然后有消息就可以自动处理了,应对高并发。接下来整理下多线程基础知识

二、多线程的三种基本实现:
1、继承Thread类,实现run方法,优点是实现简单,直接new 自定义线程类,然后start即可,缺点是扩展性不足,因为Java是单继承的语言
2、实现Runnable接口,实现run方法,对于多线程共享资源的场景,具有天然的支持,调用时略微麻烦一点点,需要先new 自定义的Runnable类,然后在new Thread(自定义Runnable实例对象)才能调用。
3、实现Callable接口,实现call方法,也是支持多线程处理同一份资源,调用时比第二种再略微麻烦一点,需要先new 自定义的Callable类,然后new FutureTask(myCallable),再new Thread (futureTask 实例),但是thread.start()后可以通过futureTask.get()来获取执行结果并且也可以在get()传参控制等待时长。在第二种基础上具有了接收返回值以及可以抛出受检查异常。并且基于第三种我们常用的Future类可以获取执行结果以及进行一些其他的操作。Future表示一个可能还没有完成的异步任务的结果,针对这个结果可以添加Callback以便在任务执行成功或失败后作出相应的操作。
    get()方法可以当任务结束后返回一个结果,如果调用时,工作还没有结束,则会阻塞线程,直到任务执行完毕
    get(long timeout,TimeUnit unit)做多等待timeout的时间就会返回结果
    cancel(boolean mayInterruptIfRunning)方法可以用来停止一个任务,如果任务可以停止(通过mayInterruptIfRunning来进行判断),则可以返回true,如果任务已经完成或者已经停止,或者这个任务无法停止,则会返回false.
    isDone()方法判断当前方法是否完成
    isCancel()方法判断当前方法是否取消
看了下get的源码,get时调用了awaitDone,awaitDone又调用了挂起LockSupport.park()、LockSupport.parkNanos()。不释放锁资源。另外目前实际应用中一般也是@Async注解与Future混合使用,使用线程池去管理Callable线程。
可以参考样例:https://www.cnblogs.com/jcjssl/p/9592145.html

三、线程状态:
1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
3. 阻塞(BLOCKED):表示线程阻塞于锁。当线程处于运行状态时,获取锁失败,线程实例进入等待队列,同时状态变为阻塞
4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。当线程处于运行状态时,线程执行了obj.wait()或Thread.join()方法、Thread.join、LockSupport.park以及Thread.sleep()时,线程处于等待状态
5. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。当线程处于运行状态时,线程执行了obj.wait(long)、Thread.join(long)、LockSupport.parkNanos、LockSupport.parkUntil以及Thread.sleep(long)方法时,线程处于超时等待状态
6. 终止(TERMINATED):表示该线程已经执行完毕。当线程执行完毕或出现异常提前结束时,线程进入终止状态

四、线程基础方法整理
1、join()。先说下为什么有join:在很多情况下,主线程创建并启动子线程,如果子线程中要进行大量的耗时运算,主线程将可能早于子线程结束。如果主线程需要知道子线程的执行结果时,就需要等待子线程执行结束了。主线程可以sleep(xx),但这样的xx时间不好确定,因为子线程的执行时间不确定,join()方法比较合适这个场景。找个join的调用小样例

测试的话我们可以看到 ,调用了 t.join() 后, 主线程main线程,一直等到该线程执行结束后,才继续执行main线程自己剩下的业务逻辑。
然后我们再看下join的源码:
  /**Waits for this thread to die.*/    
public final void join() throws InterruptedException {
        join(0);
}

/**
 * Waits at most millis milliseconds for this thread to die. A timeout of 0 means to wait forever. 
 * This implementation uses a loop of this.wait calls conditioned on this.isAlive. As a thread terminates the this.notifyAll method is invoked. It is recommended that applications not use wait, notify, or notifyAll on Thread instances.
 */
    public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

Join方法实现是通过wait(小提示:Object 提供的方法)。 当main线程调用t.join时候,main线程会获得线程对象t的锁(wait 意味着拿到该对象的锁),调用该对象的wait(等待时间),直到该对象唤醒main线程 ,比如退出后。这就意味着main 线程调用t.join时,必须能够拿到线程t对象的锁。 join() 和 join(0) 是等价的,表示一直等下去;join(非0)表示等待一段时间。从源码可以看到 join(0) 调用了Object.wait(0),其中Object.wait(0) 会一直等待,直到被notify/中断才返回。while(isAlive())是为了防止子线程伪唤醒(spurious wakeup),只要子线程没有TERMINATED的,父线程就需要继续等下去。 join() 和 sleep() 一样,可以被中断(被中断时,会抛出 InterrupptedException 异常);不同的是,join() 内部调用了 wait(),会出让锁,而 sleep() 会一直保持锁,注意这里说的是A线程调用  B.join,所以A进程会进入到B的entry队列中,并且释放A占有的关于B的资源(如果有的话)。调用时候注意join一定在start之后否则这个等待没有意义,都不知道等待谁;第二个就是如果子线程的中断,主线程并不会抛出异常。

2、yield(),一定是当前线程调用此方法,当前线程放弃获取的CPU时间片,但不释放锁资源,由运行状态变为就绪状态,让OS再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield()不会导致阻塞,因为其不释放锁。该方法与sleep()类似,只是不能由用户指定暂停多长时间。
3、Thread.sleep(long millis) 一定是当前线程调用此方法,当前线程进入TIMED_WAITING状态,但不释放对象锁,millis后线程自动苏醒进入就绪状态。作用:给其它线程执行机会的最佳方式。
4、t.join()/t.join(long millis),当前线程里调用其它线程t的join方法,当前线程进入WAITING/TIMED_WAITING状态,当前线程不会释放已经持有的对象锁。线程t执行完毕或者millis时间到,当前线程进入就绪状态。
5、obj.wait(),当前线程调用对象的wait()方法,当前线程释放对象锁,进入等待队列。依靠notify()/notifyAll()唤醒或者wait(long timeout) timeout时间到自动唤醒。
6、obj.notify()唤醒在此对象监视器上等待的单个线程,选择是任意性的。notifyAll()唤醒在此对象监视器上等待的所有线程。
7、LockSupport.park()他与Thread.sleep()相比较,都是阻塞当前线程的执行,且都不会释放当前线程占有的锁资源;区别是sleep只能时间到了自己醒过来,LockSupport.park()方法可以被另一个线程调用LockSupport.unpark()方法唤醒;sleep方法声明上抛出了InterruptedException中断异常而LockSupport.park()方法不需要捕获中断异常;
8、Object.wait()方法需要在synchronized块中执行;Object.wait()不带超时的,需要另一个线程执行notify()来唤醒,但不一定继续执行后续内容;LockSupport.park()不带超时的,需要另一个线程执行unpark()来唤醒,一定会继续执行后续内容;LockSupport.park()不带超时的,需要另一个线程执行unpark()来唤醒,一定会继续执行后续内容;如果在wait()之前执行了notify()会怎样?抛出IllegalMonitorStateException异常;如果在park()之前执行了unpark()会怎样?线程不会被阻塞,直接跳过park(),继续执行后续内容;
9、interrupted:中断可以理解为线程的一个标志位,它表示了一个运行中的线程是否被其他线程进行了中断操作。中断好比其他线程对该线程打了一个招呼。其他线程可以调用该线程的interrupt()方法对其进行中断操作,同时该线程可以调用。isInterrupted()来感知其他线程对其自身的中断操作,从而做出响应。另外,同样可以调用Thread的静态方法。interrupted()对当前线程进行中断操作,该方法会清除中断标志位。需要注意的是,当抛出InterruptedException时候,会清除中断标志位,也就是说在调用isInterrupted会返回false。
10、Daemon:thread.setDaemon(true)
public class DaemonDemo {
    public static void main(String[] args) {
        Thread daemonThread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        System.out.println("i am alive");
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        System.out.println("finally block");
                    }
                }
            }
        });
        daemonThread.setDaemon(true);
        daemonThread.start();
        //确保main线程结束前能给daemonThread能够分到时间片
        try {
            Thread.sleep(800);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
输出结果:
> i am alive
> finally block
> i am alive
上面的例子中daemodThread run方法中是一个while死循环,会一直打印,但是当main线程结束后daemonThread就会退出所以不会出现死循环的情况。main线程先睡眠800ms保证daemonThread能够拥有一次时间片的机会,也就是说可以正常执行一次打印“i am alive”操作和一次finally块中"finally block"操作。紧接着main 线程结束后,daemonThread退出,这个时候只打印了"i am alive"并没有打印finnal块中的。因此,这里需要注意的是守护线程在退出的时候并不会执行finnaly块中的代码,所以将释放资源等操作不要放在finnaly块中执行,这种操作是不安全的。线程可以通过setDaemon(true)的方法将线程设置为守护线程。并且需要注意的是设置守护线程要先于start()方法,否则会报异常,但是该线程还是会执行,只不过会当做正常的用户线程执行。

、线程间状态切换

根据四中的内容我们可以整理下,线程各个状态的切换关系,
NEW:创建线程实例然后,未调用start时候我们就已经进入了新建状态。只能新建时候进入NEW状态
RUNNABLE:1、未拿到锁也就是start()之后,获取到锁资源就进入ready,再获取到cup时间就可以运行统称为可运行。所以说:
    start()方法,从NEW进入就绪会进入RUNNABLE
    sleep(long )方法结束后会从WAITING进入RUNNABLE
    notify/notifyall会从WAITING进入RUNNABLE
    join线程执行完毕当前主线程会从WAITING进入RUNNABLE,
    LockSupport.unpark会从WAITING进入RUNNABLE
    parkNanos时间到会从WAITING进入RUNNABLE
    parkUnitl时间到会从WAITING进入RUNNABLE
    yield()线程会放弃获取的CPU时间,进入就绪状态等待线程调度重新调度,让步操作,释放CPU资源但是不释放锁资源,就绪跟运行其实都是Runnable状态。这个方法其实是内部ready与running来回切换而已。
    阻塞状态获取到锁,一样会从Blocked进入RUNNABLE
从上面可以看出从NEW、WAITING、BLOCKED都可以进入到RUNNABLE状态
WAITING/TIMEOUT WAIGTING:
    join()当主线程从RUNNABLE状态调用join后主线程就释放了子线程的对象锁,进入WAITING而子线程就进入了Runnable,但是注意join里面用的是wait()其对象锁会释放的。
    wait(),从RUNNABLE进入WAITING
    sleep(),从RUNNABLE进入WAITING
    LockSuppport.park,从RUNNABLE进入WAITING
    LockSupport.parkNanos,从RUNNABLE进入WAITING
    LockSupport.parkUntil,从RUNNABLE进入WAITING
从上面可以看出进入WAITING只能从RUNNABLE
BLOCKED:
    失去同步锁的时候就会进入Blocked状态
只有从RUNNABLE状态失去同步锁进入锁的等待队列中就进入了Blocked状态。
TERMINATED:
只有发生未处理异常获取执行完成会进入TERMINATED状态

参考:
https://blog.csdn.net/boker_han/article/details/82918881
https://www.cnblogs.com/duanxz/p/5038471.html
https://www.cnblogs.com/tong-yuan/p/11768904.html
https://www.javazhiyin.com/857.html
https://www.cnblogs.com/wangwudi/p/12302668.html   

六、JMM(Java Memory Model)

从上图我们可以看到共享变量会先放在主存中,每个线程都有属于自己的工作内存,并且会把位于主存中的共享变量拷贝到自己的工作内存,之后的读写操作均使用位于工作内存的变量副本,并在某个时刻将工作内存的变量副本写回到主存中去。JMM就从抽象层次定义了这种方式,并且JMM决定了一个线程对共享变量的写入何时对其他线程是可见的。说下我的理解:首先学习多线程编程之前建议看下java内存模型,堆、方法区、虚拟机栈、本地方法栈、程序计数器基本概念,就会对上面的图比较好了解,堆和方法区是线程间共享的,而另外三个是属于线程私有的。所以我们一般举例并发问题的时候定义一个类静态变量num,然后做累加操作。其实就是相当于num放在主内存中,然后各个线程拉取其值到自己的工作内存然后操作在刷回主内存。当我们了解JMM后也明白为什么GC主要发生在堆其次在方法区。虚拟机栈以及本地方法栈都是随着线程的创建与终止同步消亡的根本就不用GC。还有volatitle修饰的关键字,它的特点修改变量后强制刷回主内存,并且会使各个工作内存中已经获取的num直接失效,需要重新读取。第二个作用就是禁止指令重排序。其实上面说的就是 可见性,尤其在并发编程中无法保证可见性就无法保证线程安全。但是用了volatile关键字就没有安全性问题么?不会的。并发程序正确地执行,必须要保证原子性、可见性以及有序性。volatile能保证可见性以及有序性(禁止指令重排),但是无法保证原子性,如果存在原子性问题一样会出现线程安全问题。就比如我们上面举例的num++操作,这个操作并不是原子性的,这个就包括了读取当前num然后赋值再写回主内存三步。一般来说简单的读取或者赋值都是原子性,++或者a=a+1这种都是非原子性,所以上面会依旧会出现线程安全问题。,说下原理:有些人包括我自己曾经也有些晕,明明volatile已经保证可见性了,为什么其他线程还会读取到错误值,难道可见性不好使了吗?其实这个是这样的,举个例子:假如某个时刻变量num的值为10,线程1对变量进行自增操作,线程1先读取了变量num的原始值,然后线程1被阻塞了;然后线程2对变量进行自增操作,线程2也去读取变量num的原始值,由于线程1只是对变量num进行读取操作,而没有对变量进行修改操作,所以不会导致线程2的工作内存中缓存变量num的缓存行无效,也不会导致主存中的值刷新,所以线程2会直接去主存读取num的值,发现num的值时10,然后进行加1操作,并把11写入工作内存,最后写入主存。然后线程1接着进行加1操作,由于已经读取了num的值,注意此时在线程1的工作内存中num的值仍然为10,所以线程1对num进行加1操作后num的值为11,然后将11写入工作内存,最后写入主存。那么两个线程分别进行了一次自增操作后,num只增加了1。我的理解就是volatile所谓的可见性就是让线程修改后的值刷回主内存,并让其他工作内存中的变量缓存行失效。但是对于上面那种已经执行完读取操作的是无效的,只有再次读取才能感知到。这里volatile还是有作用的,如果没有volatile那么线程2修改后不能保证立即刷回主内存也不能保证其他线程的缓存行立即失效,所以这可见性还是很有作用的。但是其失败在了原子性上面。所以我们可以保证其原子性来最终用保证线程安全问题。所以一般来说volatile的应用场景也是操作一般是原子性不用额外保证其原子性的场景。

七、有序性
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,因为处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证Instruction 1会在Instruction 2之前执行。但是会影响到多线程并发执行的正确性。要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。在Java里面,可以通过volatile关键字来保证一定的“有序性”。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

八、happens-before原则
1. 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
2. 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
3. volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
4. 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
5. start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
6. join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
7. 程序中断规则:对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生。
8. 对象finalize规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始。

参考:
https://www.cnblogs.com/dolphin0520/p/3920373.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值