Java多线程/并发

1.线程的实现/创建方式

1)继承Thread,Thread本质实现了Runnable

        thread.start();

        run(){

        //线程内部业务处理

        }

2)实现Runnable,如果自己已经extends了另一个类,那就不能再继承Thread.

    Thread thread = new Thread(myThread); 

    run(){

    // 线程内部业务处理

    }

    thread.start();

3)ExecutorService、Callable、Future有返回值线程,有返回值的任务必须实现Callable,无返回值的任务必须实现Runnable.

    ExecutorServicePool pool =  Executors.newFixedThreadpool(taskSize);

4)基于线程池的方式(缓存的策略),线程的创建和销毁是非常浪费资源的。

    //创建线程池

    ExecutorService threadPool = Executors.newFixedThreadPool(10);

    while(true){

        threadPool.execute(new Runnable(){//提交多个线程任务,并执行

        @Override

        public void run(){

                        System.out.println(Thread.currentThread().getName()  + " is running ..");

            try{

                Thread.sleep(3000);

            }catch(InterruptedException e){

                e.printStackTrace;

            }

        }


        })

    }

2. 4种线程池

Java里面线程池的顶级接口是Executor,但Executor并不是线程池,只是执行的工具,真正的线程池接口是ExecutorService.

1) newCachedThreadPool

2) newFixedThreadPool,创建可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。

3)newScheduledThreadPool,可安排在给定延迟后运行命令或者定期执行。

scheduledThreadPool.schedule(newRunnable,3,TimeUnit.SECONDS);//延迟3秒

scheduledThreadPool.scheduleAtFixedRate(newRunnable,1,3,TimeUnit.SECONDS);//延迟1秒后每3秒执行一次

4)newSingleThreadExecutor

Executors.newSingleThreadExecutor()返回一个线程池(这个线程池只有一个线程),这个线程池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去。

3.线程的生命周期(5个状态)

   New(新建)--> Runnable(就绪)---> Running(运行)---> Blocked(阻塞)---> Dead(死亡)

1)New 新建状态:使用new关键字创建了线程之后,该线程就处于新建状态,此时由JVM分配内存,并初始化其成员变量的值。

2)Runnable 就绪状态:该线程对象调用了start()方法后,该线程处于就绪状态,JVM会为其创建方法调用栈和程序计数器,等待调度运行。

3)Running 运行状态:如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体。

4)Blocked 阻塞状态:阻塞是指线程因某种原因放弃了CPU使用权,也让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice转到运行(running)状态。阻塞的情况分3种:

    1.等待阻塞(o.wait->等待队列)

    运行(running)的线程执行了o.wait()方法,JVM会把该线程放入等待队列(wait queue)中。

    2.同步阻塞(lock->锁池)

     运行(running)的线程在获取该对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入线程池(lock pool)中。

    3.其他阻塞(sleep/join)

    运行(running的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(running)状态。

5)Dead(线程死亡):线程会以下面3种方式结束,结束后就是死亡状态。

    1.正常结束,run()或者call()方法执行完成,线程正常结束。

    2.异常结束,线程抛出一个未捕获的Exception或者Error.

    3.调用stop,直接调用该线程的stop()方法来结束该线程(该方法通常会容易导致死锁),不推荐使用。

4.终止线程的4种方式

1)正常结束,程序运行结束后,线程自动结束。

2)使用退出标志退出线程

    有些线程是伺服线程,需要长时间运行,只有在某种条件满足的情况下,才能关闭这些线程。使用一个变量来控制循环,比如:

    public volatile boolean exit = false;

    public void run(){

        while(!exit){

        //do something

        }

    }

3)Interrupt方法结束线程,两种情况:

    1.线程处于阻塞状态,如使用了sleep,同步锁的wait,socket中的receiver,accept等方法时,会使线程处于阻塞状态。当调用线程的interrupt()方法时,会抛出InterruptException异常。阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后break跳出循环状态,从而让我们有机会结束这个线程的执行。通常很多人认为只要调用interrupt方法线程就会结束,实际上是错的, 一定要先捕获InterruptedException异常之后通过break来跳出循环,才能正常结束run方法。 

    2.线程未处于阻塞状态,使用isInterrupted()判断线程的中断标志来退出循环。当使用interrupt()方法时,中断标志就会置true,和使用自定义的标志来控制循环是一样的道理。 

4)stop方法终止线程(线程不安全)

    程序中可以直接使用thread.stop()来强行终止线程,但是stop方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果。    

5.sleep与wait的区别

    1)sleep()属于Thread类中,wait()属于Object中的方法。

   2)sleep()方法导致了程序暂停执行指定的时间,让出cpu给其他线程,但是它的监控状态依然保持着当指定的时间到了又会自动恢复运行状态。

   3)在调用sleep()方法的过程中,线程不会释放对象锁。

    4)当调用wait()方法的时候,线程会放弃对象锁,进入等待对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备获取对象进入运行状态。

6.start与run的区别

1)start()方法启动线程,真正实现了多线程运行,这时无需等待run方法体代码执行完毕,可以直接继续执行下面的代码。

2)通过调用Thread类的start()方法来启动一个线程,这时此线程是处于就绪状态,并没有运行。

3)方法run()称为线程体,它包含了要这行这个线程的内容,线程进入了运行状态,开始运行run函数体当中的代码。

7.Java后台线程

1)定义:守护线程--也叫服务线程,为用户线程提供公共服务,在没有用户线程可服务时会自动离开。

2)优先级:守护线程优先级比较低。

3)设置:通过setDaemon(true)来设置线程为守护线程。

4)在Daemon线程中产生的新线程也还是Daemon线程。

5)Example:垃圾回收线程是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。 

6)生命周期:依赖于系统,与系统“同生共死”。当JVM中所有的线程都是守护线程的时候,JVM就可以退出了;如果还有一个或以上的非守护线程则JVM不会退出。 

8.JAVA锁

1)乐观锁:认为多读少写,遇到并发写的可能性低,每次去拿数据都认为别人不会修改,所以不会上锁。但在更新的时候会判断一下在此期间别人有没有更新这个数据,采取在写时先读取当前版本,然后加锁操作。

   JAVA中的乐观锁基本都是CAS实现,比较当前值跟传入值是否一样,一样则更新,否则失败。

2)悲观锁:认为写多,遇到并发写的可能性高,每次拿数据都认为别人会修改,所以在读写数据的时候都会上锁。Java中的悲观锁就是Synchronized,AQS框架下的锁是先尝试cas乐观锁去获取,获取不到,才会转为悲观锁,如RetreenLock.

3)自旋锁:原理非常简单-如果持有锁的线程能在很短的时间内释放锁,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等待持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换消耗。

    因此,自旋锁有一个自旋等待的最大时间,如果持有锁线程执行的时间超过了这个设置的自旋等待的最大时间,这时争用线程会停止自旋进入阻塞状态。

    自旋锁的优点:尽可能减少线程的阻塞,对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能大幅提升。因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗。

    如果锁的竞争激烈或者持有锁的线程需要长时间占用锁执行同步快,就不适合自旋锁。

    自旋锁时间阀值,自旋锁的目的是为了占着CPU的资源不释放,等到获取到锁立即执行处理。

    自旋锁的开启:JAVA1.6中-XX:UseSpinning开启;-XX:PreBlockSpin = 10为自旋次数;JDK1.7后,去掉此参数,由jvm控制。

4)Synchronized同步锁:可以把任意一个非NULL的对象当作锁。它属于独占式悲观锁,同时属于可重入锁。

Synchronized作用范围:

    1.作用于方法上,锁住的是对象的实例(this)

    2.作用于静态方法上,锁住的是Class实例,又因为Class的相关数据存储在永久代PermGen(jdk1.8是metaspace),永久代是全局共享的,因此静态方法锁相当于类的全局锁,会锁所有调用该方法的线程。

    3.作用于一个对象实例,锁住的是所有以该对象为锁的代码块。

5)ReentrantLock:继承接口Lock并实现接口中定义的方法,可重入锁,除了能完成synchronized所能完成的工作外,还可以响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。

Lock接口的主要方法:

    1. void lock():执行此方法,如果锁处于空闲状态,当前线程就获取到锁。

    2. boolean tryLock():如果锁可用,则获取锁,并立即返回true,否则返回false.

    3. void unlock():当前线程释放持有的锁,

    4. Condition newCondition():条件对象,获取等待通知组件。

    5. getHoldCount():查询当前线程保持此锁的次数,也就是执行此线程执行lock方法的次数。

    6. getQueueLength():返回正等待获取此锁的线程估计数,比如启动10个线程,1个线程获得锁,此时返回的是9.

    7. getWaitQueueLength:(Condition condition)返回等待与此锁相关的给定条件的线程估计数。比如10个线程,用同一个condition对象,并且此时这10个线程都执行了condition对象的await方法,那么此时执行此方法返回10 

    8. hasWaiters(Condition condition):查询是否有线程等待与此锁有关的给定条件(condition),对于指定contidion对象,有多少线程执行了condition.await方法 

    9. hasQueuedThread(Thread thread):查询给定线程是否等待获取此锁 

    10. hasQueuedThreads():是否有线程等待此锁 

    11. isFair():该锁是否公平锁 

    12. isHeldByCurrentThread(): 当前线程是否保持锁 锁定,线程的执行lock方法的前后分别是false和true 

    13. isLock():此锁是否有任意线程占用 

    14. lockInterruptibly():如果当前线程未被中断,获取锁 

    15. tryLock():尝试获得锁,仅在调用时锁未被线程占用,获得锁 

    16. tryLock(long timeout TimeUnit unit):如果锁在给定等待时间内没有被另一个线程保持,则获取该锁。 

6)非公平锁:JVM按随机、就近原则分配锁的机制称为不公平锁,ReentrantLock在构造函数中提供了是否公平锁的初始化,默认非公平锁;非公平锁实际执行的效率远远超出公平锁,除非程序有持续性需要。

    加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待。

    1. 非公平锁性能比公平锁高5~10倍,因为公平锁需要在多核的情况下维护一个队列 

    2. Java中的synchronized是非公平锁,ReentrantLock 默认的lock()方法采用的是非公平锁。 

7)公平锁:指锁的非配机制是公平的,通常先对锁提出获取请求的线程会先被分配到锁,ReentrantLock在构造函数中提供了是否公平锁的初始化方式来定义公平锁。 

    加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得 

8)ReentrantLock与synchronized

    1.ReentrantLock通过方法lock()与unlock()来进行加锁与释放锁,与synchronized被JVM自动解锁机制不同,ReentrantLock加锁后需要手动解锁。为了避免程序出现异常而无法正常解锁的情况,使用ReentrantLock必须在finally控制块中进行解锁操作。

    2.ReentrantLock相比synchronized的优势是可中断、公平锁、多个锁。

ReentrantLock的实现:

public class MyService{

    private Lock lock = new ReentrantLock();

    //new ReentrantLock(false);//非公平锁

    //new ReentrantLock(true);//公平锁

    private Condition condition = lock.newCondition();

    public void test(){

        try{

            lock.lock();//加锁

            //1.wait等待

            //System.out.println("wait");

            condition.await();

            //2.signal唤醒

            condition.signal();

            for(int i = 0; i < 5 ; i++){

                System.out.println("ThreadName=" + Thread.currentThread().getName()+ (" " + (i + 1)));

            }

        }catch (InterruptedException e){

        }finnaly{

            lock.unlock();

        }

    }

}

9)Condition类和Object类锁方法区别

    1.Condition类的await()等价于Object类的wait()

    2.Condition类的signal()等价于Object类的notify()

    3.Condition类的signalAll()等价于Object类的notifyAll()

    4.ReentrantLock可以唤醒指定条件的线程,而object的唤醒是随机的。

10)tryLock和lock和lockInterruptibly区别

    1.tryLock能获得锁就返回true,不能就立即返回false,tryLock(long timeout,TimeUnit unit),可以增加时间限制,如果超过该时间段还没获得锁,返回false.

    2.lock能获得锁就返回true,不能的话就一致等待获得锁。

    3.lock和lockInterruptibly,如果两个线程分别执行这两个方法,但此时中断这两个线程,lock不会抛出异常,而lockInterruptibly就会抛出异常。

11)了解Semaphore,几乎与ReentrantLock相当,能完成ReentrantLock的所有功能

12)AtomicInteger,一个提供原子操作的Integer类,常见的还有AtomicBoolean、AtomicLong、AtomicReference等。原理相同,AtomicReference可以将一个对象的所有操作转为原子操作。

    再多线程中,诸如++i或者i++等运算不具有原子性,是不安全的线程操作之一,JVM提供Atomic..替代synchronized或者ReentrantLock,使效率变得更高。

12)可重入锁(递归锁):指同一线程外层函数获得锁之后,内层递归函数仍然有获取该锁的代码,但不受影响。在JAVA环境下 ReentrantLock 和synchronized 都是 可重入锁。 

13)ReadWriteLock读写锁:为了提高性能,Java提供了读写锁,在读的地方使用读锁,写的地方使用写锁,灵活控制。读的时候上读锁,写的时候上写锁。

14)共享锁和独占锁:

    独占锁:每次只能有一个线程能持有锁,ReentrantLock就以独占的方式实现的互斥锁。是一种悲观加锁策略,避免了读/读冲突。如果某个只读线程获取锁,则其他读线程只能等待,这种情况下限制了不必要的并发性,因为读操作不影响数据的一致性。

    共享锁:允许多个线程同时获取锁,并发访问共享资源,如ReadWriteLock,共享锁是一种乐观锁,放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。

15)重量级锁(Mutex Lock)

    依赖于操作系统的Mutex Lock所实现的锁,Java的synchronized多次优化就是为了减少这种重量级锁的使用。

16)轻量级锁

    锁的状态总共有4种:无锁状态、偏向锁、轻量级锁和重量级锁。

    锁升级:随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级到重量级锁(但是只能从低到高,不能降级)。

    轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。 

17)偏向锁:偏向锁的目的是在某个线程获得锁之后,消除这个线程锁重入(CAS)的开销,看起来让这个线程得到了偏护。

18)分段锁:并非一种实际的锁,而是一种思想(ConcurrentHashMap是学习分段锁的最好实现)

19)锁优化:

    1.减少锁持有时间:只有在有线程安全要求的程序上加锁。

    2.减少锁粒度:将大对象折成小对象,大大减少并行度,降低锁竞争。降低锁的竞争,偏向锁、轻量级锁成功率才会提高。

    3.锁分离:最常见的锁分离就是读写锁   ReadWriteLock,根据功能进行分离成读锁和写锁。这样读读不互斥,读写互斥,写写互斥,既保证了线程安全又提高了性能。

     4.锁粗化:通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完公共资源后,应该立即释放锁。但是,凡事都有一个度,如果对同一个锁不停的进行请求、同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化 。 

    5.锁消除:是在编译器级别的事情。在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作,多数是因为程序员编码不规范引起。 

9.线程基本方法

线程的基本方法有:wait notify notifyAll sleep join yield等

1)线程等待(wait):调用wait方法后会释放对象锁,线程进入waiting状态,需要其他线程唤醒。

2)线程睡眠(sleep):线程进入休眠状态,不释放锁,sleep(long)线程进入timed-waiting状态。

3)线程让步(yield):线程让出cpu-timeslice,与其他线程一起竞争cpu-timeslice,一般优先级高的有更大的可能性成功竞争到cpu-timeslice.

4)线程中断(interrupt):中断线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。这个线程本身并不会因此而改变状态(如阻塞,终止等)。

    1.调用inerrupt()方法并不会中断一个正在运行的线程。

    2.若调用sleep()而线程处于timed-waiting状态,这时调用interrupt()方法,会抛出InterruptedException,从而使线程提前结束timed-waiting状态。

    3.许多声明抛出InterruptedException的方法(如Thread.sleep(long mills)),抛出异常前,都会清除中断标识位,所以抛出异常后,调用isInterruped()方法将会返回false.

    4.中断状态是线程固有的一个标识位,可以通过此标识位安全的终止线程

5)等待其他线程终止(join):在当前线程中调用一个线程的join(),则当前线程转为阻塞状态,回到另一个线程结束,当前线程再由阻塞变为就绪状态,等待cpu.

    join()的使用场景:主线程生成并启动了子线程,需要用到子线程返回的结果,也就是需要主线程等待子线程结束后再结束,这时调用join().

6)线程唤醒(notify):唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象上等待,则会选择唤醒其中一个,任意选择。类似的方法还有notifyAll(),唤醒在此监视器上等待的所有线程。

7)其他方法:

    1.isActive():判断一个线程是否存活;

    2.activeCount():程序中活跃的线程数;

    3.enumerate():枚举程序中的线程;

    4.currentThread():得到当前线程;

    5.isDaemon():是否为守护线程;

    6.setDaemon():设置线程为守护线程;

    7.setName():为线程设置一个名字;

    8.setPriority():设置线程的优先级;

    9.getPriority():获得线程的优先级

10.线程池

1)线程池原理:线程池的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。它的主要特点是:线程复用,控制最大的并发量;管理线程。

2)组成(四部分):线程池管理器(用于创建并管理线程池)、工作线程(线程池中的线程)、任务接口(每个任务必须实现的接口,用于工作线程调度其运行)、任务队列(用于存放待处理的任务,提高一种缓冲机制)

3)ThreadPoolExecutor的构造方法:

    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue workQueue){                this(corePoolSize,maximumPoolSize,keepAliveTime,unit,workQueue,Excutors.defaultThreadFactory(),defaultHandler);

   }

    1.corePoolSize:指定了线程池中的线程数量;

    2.maximumPoolSize:指定了线程池中最大线程数量;

    3.keepAliveTime:当前线程池数量超过corePoolSize时,多余的空闲线程的存活时间,即多长时间内会被销毁;

    4.unit:keepAliveTime的单位;

    5.workQueue:任务队列,被提交但尚未被执行的任务;

    6.threadFactory:线程工厂,用于创建线程,一般默认即可;

    7.handler:拒绝策略,当任务太多来不及处理,如何拒绝任务;

4)拒绝策略:线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列也已经排满,再也塞不下新的任务,这时需要拒绝策略合理的处理这个问题。JDK内置的拒绝策略:

    1.AbortPolicy:直接抛出异常,阻止系统正常运行;

    2.CallerRunsPolicy:只要线程池未关闭,该策略直接在调用者线程中运行当前被丢弃的任务。

    3.DiscardOldestPolicy:丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务。

    4.DiscardPolicy:该策略默默的丢弃无法处理的任务,不予任何处理。如果允许任务丢失,这是最好的方案。

    5.以上均实现RejectedExecutionHandler,如果策略无法满足需求则可自己扩展RejectedExecutionHandler接口。

5)线程池工作过程:

    1.线程池刚创建时,里面没有一个线程。

    2.当调用execute()方法添加一个任务时,线程池会做如下判断:

        a)如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;

        b)如果正在运行的线程数大于或等于corePoolSize,那么将这个任务放入队列;

        c)如果这个时候队列满了,而且正在运行的线程数量小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;

        d)如果队列满了,而且正在运行的线程数大于或等于maximumPoolSize,那么线程池会抛出异常RejectExecutionException.

    3.当一个线程完成任务时,它会从队列中取下一个任务来执行。

    4.当一个线程无事可做,超过一定时间(keepAliveTime)时,线程池会判断,如果当前线程数大于corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。

11.线程计数器(CountDownLatch)

使用场景:例如有一个任务A,它要等待其他4个任务执行完毕后才能执行

12.CyclicBarrier(回环栅栏-等待至barrier状态再全部同时执行)

1.字面意思回环栅栏,通过它可以实现让一组线程等待至某个状态之后再全部同时执行。    

2.CyclicBarrier中最重要的方法:

    1)public int await():用来挂起当前线程,直至所有线程都到达barrier状态再同时执行后续任务;

    2)public int await(long timeout,TimeUnit unit):让这些线程等待至一定时间,如果还有线程没有到达barrier状态就直接让到达barrier的线程执行后续任务。

13.什么是AQS(抽象的队列同步器)?

    AbstractQueuedSynchronizer,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch。 

14.什么是CAS(比较并交换-乐观锁机制-锁自旋)

    CAS(Compare And Swap/Set)比较并交换,CAS算法的过程是这样:它包含3个参数CAS(V,E,N)。V表示要更新的变量(内存值),E表示预期值(旧的),N表示新值。当且仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值。 

15.ABA问题

    CAS会导致“ABA”问题,CAS算法实现一个重要前提需要取出内存中某时刻的数据,而在下时刻比较并替换,那么在这个时间差类会导致数据的变化。部分乐观锁的实现是通过版本号(version)的方式来解决ABA问题。

16.多个线程共享数据和ThreadLocal(略)

你可能感兴趣的:(Java多线程/并发)