java并发编程及juc包的应用

多线程:

        进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位。windows操作系统是多任务操作系统,它以进程为单位,一个进程是一个包含有自身地址的程序,每个正在独立执行的程序都称为进程,每个进程有自己的内存空间和系统资源。系统可以分配给每个进程一段有限的使用CPU时间,CPU在这个时间片中执行某个进程,然后下一个时间片又跳到另一个进程中去执行,由于CPU转换较快,所以使的每个进程好像同时进行,多核CPU的多核同时运行,但是进程数远远大于核数,所以还是要依靠切换进程来做。一个线程则是进程中的执行流程,一个进程中可以同时包括多个线程,每个线程也可以得到一小段程序的执行时间,这样一个进程就可以具有多个并发执行的线程,在单线程中,程序的代码按照调用顺序依次往下执行,如果需要一个进程同时完成多段代码的操作,就需要产生多线程(多条执行路径)。多线程有更高的几率抢到CPU的执行权,可以提高应用程序的使用率,但我们不能保证哪一个线程能够在哪一个时刻抢到,所以线程的执行有随机性。在并发编程中,我们通常会遇到以下三个问题:原子性问题,可见性问题,有序性问题。

并行和并发:前者是逻辑上同时发生,旨在某一个时间内同时运行多个程序(后端线程模型),后者是物理上同时发生,指在某一个时间点同时运行多个程序(客户端)。

线程的两种调度模型:平均分配每个线程占用CPU的时间片,上面说的就是这种,第二种就是抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,会随机选一个,优先级高仅仅表示线程获取的cpu时间片的几率相对对多些,也就是说不是高的执行完再执行低的,需要多多次运行才能看到效果。java用的就是抢占式。

优先级:Java中每个线程都有优先级,默认情况下,新建线程的优先级与创建该线程的优先级相同,每当线程调度器选择要运行的线程时,通常选择优先级较高的线程,设置线程的优先级线程对象。setpriority(int 值)值在Thread。MIN_PRIORITY和Thread。MAX_PRIORITY之间, 一般默认为Thread。NORM_PRIORITY ,如果不在1-10之间产生一个IllegalArgumentException异常。任何时候不要依赖线程优先级,因为各个操作系统的线程调度器实现大相径庭,因此依赖jdk的线程优先级来设置线程优先级策略的方法是错误的和非平台可移植的。

多线程的错误问题:下面本改1000 *10 ,实际却是9994.多线程修改一个变量会出现错误,所以出现多线程编程。

Java多线程实现方式主要有四种:其中前两种方式线程执行完后都没有返回值,后两种是带返回值的,也可以用匿名内部类的方式实现多线程。

1.继承Thread类:Thread类本质上是实现了Runnable接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过Thread类的start()实例方法。start()方法会调用一个native方法start0,它将启动一个新线程,并执行run()方法。这种方式实现多线程很简单,通过自己的类直接extends Thread,并复写run()方法,就可以启动新线程并执行自己定义的run()方法。但是如果不通过start()启动线程,直接调用run方法,那么跟直接调用一个普通方法没有区别。如果start()方法调用一个已经启动的线程,系统将抛出java.lang.IllegalThreadStateException异常。主方法线程启动由java虚拟机负责,我们自己启动自己的线程。Thread里的currentThread可以返回当前正在执行的线程对象。

2.实现Runnable接口:如果自己的类已经extends另一个类,就无法直接extends Thread,此时,可以实现一个Runnable接口。另一种情况是它适用于多个相同程序的代码去处理同一个资源的情况,把线程同程序的代码,数据有效的分离,换句话说就是只需要创建一个Runnable对象,可以通过这个Runnable对象创建多个线程对象,并且多个线程对象公用一个Runnable对象数据。分为三步:1.建立Runnable对象(一个自定义的类继承了Runnable接口,并实现了run()方法)。 2.使用参数为Runnable 对象的构造方法创建 Thread对象,3.用Thread对象调用start方法启动线程。其实上面的Thread类就是实现了Runnable接口,其中run()方法正是对Runnable接口中run()方法的具体实现。

3.实现Callable接口通过FutureTask包装器来创建Thread线程,一个泛型接口,这个泛型其实就是这个接口的Call方法的返回值类型。这个call方法相当于上面run方法,而区别就是这个有泛型的返回值。Future 表示异步计算的结果,Future 的get方法的返回值为可以返回泛型V,但是如果get的时候任务还没有执行完,就会会一直等到任务完成,形成了阻塞。在get的时候可以设置时间,如果超出时间为获得结果会抛出异常,java.util.concurrent.TimeoutException。cancel(boolean mayInterruptIfRunning):取消子任务的执行,如果这个子任务已经执行结束,或者已经被取消,或者不能被取消,这个方法就会执行失败并返回false;如果子任务还没有开始执行,那么子任务会被取消,不会再被执行;如果子任务已经开始执行了,但是还没有执行结束,根据mayInterruptIfRunning的值,如果mayInterruptIfRunning = true,那么会中断执行任务的线程,然后返回true,如果参数为false,会返回true,不会中断执行任务的线程。isCancelled(),判断任务是否被取消,如果任务执行结束(正常执行结束和发生异常结束,都算执行结束)前被取消,也就是调用了cancel()方法,并且cancel()返回true,则该方法返回true,否则返回false.所以cancel(true)方法,只是调用t.interrupt(),此时,如果t因为sleep(),wait()等方法进入阻塞状态,那么阻塞的地方会抛出InterruptedException;如果线程正常运行,需要结合Threadinterrupted()方法进行判断,才能结束,否则,cancel(true)不能结束正在执行的任务。
这也就可以解释一个问题,有的情况下,使用 futuretask.cancel(true)方法并不能真正的结束子任务执行,需要配合!Thread.currentThread().isInterrupted();并且cancel后只是取消执行未执行的任务,线程池还是在运行。想要关闭线程池要用shutdown和awaitTermination。如:

ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
TimerTask timertask = new TimerTask();
ScheduledFuture<?> future = executor.scheduleAtFixedRate(timertask, 1, 1, TimeUnit.SECONDS);
Thread.sleep(3000);
if (future != null)
	future.cancel(false);
executor.shutdownNow();
while (!executor.awaitTermination(1, TimeUnit.SECONDS));

class TimerTask implements Runnable
{
	@Override
	public void run()
	{
		try
		{
			if(!Thread.currentThread().isInterrupted())
				System.out.println(1111);
		}
		catch (Throwable t)
		{		
			System.exit(0);
		}
	}
}

4.使用ExecutorService、Callable、Future实现有返回结果的多线程。可返回值的任务必须实现Callable接口。类似的,无返回值的任务必须实现Runnable接口。submit的时候其实就是执行了一个FutureTask的子类对象。如果超过线程数量就等待其他完成。

线程休眠:在指定的毫秒数内当前执行的线程暂停执行,Thread.Sleep(毫秒)。

sleep与wait区别:调用wait方法后可以使该线程从运行状态进入就绪状态,而sleep方法也可以达到 这个效果,区别在于调用sleep方法后线程不释放锁,但是用wait()方法的线程释放锁。wait(time)这种形式与sleep都是指在此时间内暂停,wait()方法永久等待下去直到notify或者notifyall,sleep的线程睡眠到期自动苏醒 ,并返回到就绪状态,不是运行状态。sleep和wait后在哪里沉睡就在那里唤醒,sleep()是静态方法,只能控制当前正在运行的线程。

wait、notify、notifyAll 是object中的方法,notify唤醒任意一个wait后等待的线程,notifyAll是唤醒所有等待的线程,注意唤醒所有之后如果没有获取到锁就会等待锁释放后继续竞争锁,跟没唤醒有本质区别。

永远不要在循环外调用wait:原因:消费者和生产者的例子。生产者满了之后wait,通知消费者,消费者消费之后nofity生产者,生产者线程是从wait处开始往下执行的,所以会执行while再次检测条件,发现条件不满足则会添加元素!如果不用while改用if:错误情况一:如果有两个生产者A和B,一个消费者C。当存储空间满了之后,生产者A和B都被wait,进入等待唤醒队列。当消费者C取走了一个数据后,如果调用了notifyAll(),注意,此处是调用notifyAll(),则生产者线程A和B都将被唤醒,如果此时A和B中的wait不在while循环中而是在if中,则A和B就不会再次判断是否符合执行条件,都将直接执行wait()之后的程序,那么如果A放入了一个数据至存储空间,则此时存储空间已经满了;但是B还是会继续往存储空间里放数据,错误便产生了。错误情况二:如果有两个生产者A和B,一个消费者C。当存储空间满了之后,生产者A和B都被wait,进入等待唤醒队列。当消费者C取走了一个数据后,如果调用了notify(),则A和B中的一个将被唤醒,假设A被唤醒,则A向存储空间放入了一个数据,至此空间就满了。A执行了notify()之后,如果唤醒了B,那么B不会再次判断是否符合执行条件,将直接执行wait()之后的程序,这样就导致向已经满了数据存储区中再次放入数据。错误产生。

wait和await的区别,wait在object中,await在condition中。两者在使用上也是非常类似,都需要先获取某个锁之后才能调用,而不同的是 Object wait,notify 对应的是 synchronized 方式的锁,Condition await,singal 则对应的是 ReentrantLock (实现 Lock 接口的锁对象)对应的锁。但这里面有一个最大的问题就是 synchronized 方式对应的 wait, notify 不能有多个谓词条件,Lock 对应的 Condition await, signal 则可以有多个谓词条件。没有多个谓词条件带来的问题在于,例如队列已满,所有的生产者现场阻塞,某个时刻消费者消费了一个元素,则需要唤醒某个生产者线程,而通过 Object notify 方式唤醒的线程不能确保一定就是一个生产者线程,因为 notify 是随机唤醒某一个正在该 synchronized 对应的锁上面通过 wait 方式阻塞的线程,如果这时正好还有消费者线程也在阻塞中,则很可能唤醒的是一个消费者线程.与之不同的 Condition await, signal 方式则可以对应多个谓词条件(notEmpty, notFull),可以很方便的实现让生产者线程和消费者线程分别在不同的谓词条件上进行等待。

线程的加入:如果当前程序为多线程时,假如存在一个线程A,现在需要插入线程B,并要求线程B先执行完毕,然后在继续执行线程A,此时可以使用Tread类中的Join方法来来完成,当某个线程使用join()方法加入到另一个线程时,另一个线程会等待该线程执行完毕再继续执行。

thread类中提供了一种礼让方法,使用yield()方法表示,它只是给当前正处于运行状态下的线程一个提醒,告知它可以将资源礼让给其他线程,但这仅仅使一种暗示,没有任何一种机制保证当前线程会将资源礼让。

后台线程:thread类中提供了setDaemon()方法可以把该线程标记为守护线程或用户线程,当正在运行的线程都是守护线程时,java虚拟机退出,该方法必须在启动该线程前调用,可以实现主线程结束,其他线程停止需求。

中断线程: thread对象调用interrupt(),通知线程应该中断了,注意:调用interrupt()方法并不会使得线程中断,而是使得线程的中断标志置为true,需要被调用的线程配合中断,在正常运行任务时,经常检查本线程的中断标志,如果被设置了中断标志就自行停止线程.调用后会有两种情况:

1.如果线程处于被阻塞状态,那么该线程将立即退出被阻塞状态,并且抛出一个InterrupedException异常.

2.如果线程初一正常活动状态,那么会将该线程的中断标志设置为true.被设置中断标志的线程将继续中场运行,不受影响.

stop、suspend、resume被抛弃的原因:

stop方法会立即释放所有他锁住对象上的锁。这会导致对象处于不一致的状态。假如一个方法在将钱从一个账户转移到另一个账户的过程中,在取款之后存款之前就停止了。那么现在银行对象就被破坏了。因为锁已经被释放了。当线程想终止另一个线程的时候,它无法知道何时调用stop是安全的,何时会导致对象被破坏。所以这个方法被弃用了。suspend()方法就是将一个线程挂起(暂停),resume()方法就是将一个挂起线程复活继续执行。suspend方法不会破坏对象,但是调用suspend()的时候,目标线程会停下来,但却仍然持有在这之前获得的锁,没任何线程可以使用这个锁住的资源直到suspend的目标线程被resumed,如果一条线程将去resume目标线程之前尝试持有这个重要的系统资源再去resume目标线程,这两条线程就相互死锁了。

此 时,其他任何线程都不能访问锁定的资源,除非被"挂起"的线程恢复运行。对任何线程来说,如果它们试图使用任何一个锁定的资源,又想恢复目标线程就会造成死锁。

线程可以有6种状态, New(新创建)Runnable(可运行状态:调用start后)Blocked(被阻塞)Waiting(等待)Timed waiting(计时等待)Terminated(被终止)要确定一个线程的状态可以调用getState方法。

因为cpu的一次操作是原子性的,所以有可能出现多个窗口卖电影票案例中相同的票被卖了多次情况。由于随机和延迟又导致了出现负票,所以引入了线程安全机制。出现问题的根本原因:1.多线程环境,2.共享数据,3.多条语句操作操作共享数据(非原子性)。

线程同步机制;多线程程序,会发生线程抢占资源问题,所以java提供了线程同步机制来防止资源冲突。例如火车票购买系统。解决方法就是给定时间内只允许一个线程访问共享资源,这时需要给资源上一道锁,就好比一个人上洗手间,一个人进去之后将门上锁,等他出来再将锁打开,然后其他人才可以进入。

1.同步块:Java的同步机制使用 synchronized关键字。用synchronized创建同步快 这个同步快也称临界区,语法格式synchronized(Object){...}.将共享资源的操作放置在synchronized定义区域内,这样当其他其他线程获取到这个锁时,必须等待锁被释放时才能进入该区域,同步可以解决安全问题的根本原因就在那个对象上,该对象如同锁的功能,Object为任意一个非null对象,每个对象都有一个标志位,如果为0状态,表明此同步块中存在其他线程在运行,这时线程处于就绪状态,直到处于同步快中的线程执行完同步快中的代码为止,这时该对象的标识被设置为1.该线程才能执行同步块中代码,并将object对象设置为0,防止其他线程执行同步快中代码,因此要多个线程用同一把锁,所以要把Object代表的任意对象用同一个非null对象。弊端:当线程相当多时,因为每个线程都会去判断同步的锁,很耗费资源,降低了程序运行效率。这里需要注意的是,synchronized后面括号里的一定要是引用类型。比如可以是Integer 类型,但不能是int。很简单,基本类型都是传值使用当然无法满足需要了。有兴趣的可以试下 int,发现会有编译错:int is not a valid type's argument for the synchronized statemen

2.同步方法:同步方法就是在方法前面修饰synchronized关键字的方法,语法格式synchronized void f(){}。当某个对象调用了同步方法时,该对象上的其他同步方法必须等待该同步方法执行完毕才能被执行,必须将每个能访问共享资源的方法修饰为synchronized。否则会出错。同步方法锁的对象是this。静态方法锁的对象是类的字节码文件(类名.class返回的对象)。如果同步块中调用同步方法, 那么两者的锁要一致。

从上图可知static synchronized的方法和非static的synchronized方法并没有互相等待。总结:对于类锁synchronized static,是通过该类直接调用加类锁的方法,而对象锁是创建对象调用加对象锁的方法,两者访问是不冲突的。只有两个static synchronized方法和同一个对象锁住的非static的synchronized方法会有访问等待。非synchronized的方法任意线程都可访问。

 一个日本作者-结成浩的《java多线程设计模式》有这样的一个列子:
      pulbic class Something(){
         public synchronized void isSyncA(){}
         public synchronized void isSyncB(){}
         public static synchronized void cSyncA(){}
         public static synchronized void cSyncB(){}
     }
   那么,假如有Something类的两个实例x与y,那么下列组方法何以被1个以上线程同时访问呢
   a.   x.isSyncA()与x.isSyncB() 
   b.   x.isSyncA()与y.isSyncA()
   c.   x.cSyncA()与y.cSyncB()
   d.   x.isSyncA()与Something.cSyncA()
    这里,很清楚的可以判断:
   a,都是对同一个实例的synchronized域访问,因此不能被同时访问
   b,是针对不同实例的,因此可以同时被访问
   c,因为是static synchronized,同一个类型锁,所以不同实例之间仍然会被限制,相当于x.isSyncA()与   x.isSyncB()了,因此不能被同时访问。
   那么,第d呢?,书上的 答案是可以被同时访问的,答案理由是synchronzied锁的是当前对象与static synchronzied锁的是字节码文件class对象,锁不同的原因。

同步的问题:1.效率低 2.如果出现了同步嵌套,就容易产生死锁问题,项目可以统一规定大锁包小锁,比如帮派锁包玩家锁。

同步过程中的死锁问题:指两个或两个以上的线程在执行过程中,因争夺资源产生的一种互相等待的现象,换句话说就是我需要你的数据,你需要我的数据。

3.lock:Jdk5以后出现的新的锁对象。lock()方法获取锁(在执行需要同步的代码前调用),unlock()方法释放锁(在执行完需要同步的代码后调用)。ReentrantLock是lock的实现类,是一种可重入锁。最好配合try。。finally 使用, 释放放在finally里调用。适用在实现runnable时共用一个对象创建多线程时,在实现类里定义一个lock字段即可。Lock obj = new ReentrantLock()obj .lock()  obj .unlock()。 就算在try中return,finally中的最后也会执行,也就是说,当执行到前面的return时,如果有finally,则先执行finally。执行完毕后继续返回前面的return继续执行,但这种情况在finally里修改要返回的值,则不能影响前面return返回的值,如果finally里也有return,则不在返回到前面的return语句,也就是说直接执行finally里的return,可以影响返回值。
 

而tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回,在拿不到锁时也不会一直在那等待。
tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。所以,一般情况下通过tryLock来获取锁时是这样使用的:

lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,这个线程能够响应中断,即中断线程的等待状态。也就说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B还是在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程,并抛出一个InterruptedException。lock 优先考虑获取锁,待获取锁成功后,才响应中断。lockInterruptibly 优先考虑响应中断,而不是响应锁的普通获取或重入获取。
由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出InterruptedException。因此lockInterruptibly()一般的使用形式如下:

synchronized 和lock 的区别:

1.首先synchronized是java内置关键字,在jvm层面,Lock是个java类;

2.synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;

3.synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;

4.synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平可非公平(两者皆可)

如果使用 synchronized ,如果A不释放,B将一直等下去,不能被中断,

如果 使用ReentrantLock,如果A不释放,可以使B在等待了足够长的时间以后,中断等待,而干别的事情。

5.Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。

Condition 控制线程通信 ,Condition 接口描述了可能会与锁有关联的条件变量。这些变量在用法上与使用 Object.wait 访问的隐式监视器类似,但提供了更强大的功能。需要特别指出的是,单个 Lock 可能与多个 Condition 对象关联。为了避免兼容性问题,Condition 方法的名称与对应的 Object 版本中的不同。在 Condition 对象中,与 wait、notify 和 notifyAll 方法对应的分别是await、signal 和 signalAll。Condition对象是需要关联Lock对象的,也就是说Condition的使用是需要依赖Lock对象的。要为特定 Lock 实例获得Condition 实例,请使用其 newCondition() 方法。Lock和Condition结合应用以实现线程按序交替。await、signal ,signalAll在调用前要获取对应的lock锁。经典案例就是有界队列。

同步:行一个操作之后,等待结果,然后才继续执行后续的操作。

异步:执行一个操作后,可以去执行其他的操作,然后等待通知再回来执行刚才没执行完的操作。

阻塞:进程/线程给CPU传达一个任务之后,一直等待CPU处理完成,然后才执行后面的操作。

非阻塞:进程/线程给CPU传达任我后,继续处理后续的操作,隔断时间再来询问之前的操作是否完成。这样的过程其实也叫轮询。

经典例子:

1.老张把水壶放到火上,立等水开(同步阻塞).

2.老张把水壶放到火上,去客厅看电视,时不时去看水开了没有(同步非阻塞)。

有一个响水壶,水开了后,能通知老张。

3.老张把响水壶放到火上,立等水开。(异步阻塞)

4.老张把响水壶放在火上,去客厅看电视,水壶响了之后通知老张去拿水壶(异步非阻塞)。

所谓同步异步,只是对水壶而言,普通水壶是同步的。响水壶是异步的,但响水壶会等自己完成主动通知老张,同步只能让调用者去轮询自己,造成效率低下,所谓阻塞非阻塞,仅仅对于老张而言,立等的老张,阻塞。看电视的老张,非阻塞。一般异步与非阻塞配合使用,否则没意义。

原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。一个很经典的例子就是银行账户转账问题,比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。

可见性:可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

有序性:即程序执行的顺序按照代码的先后顺序执行。

Java JUC简介

         在 Java 5.0 提供了 java.util.concurrent (简称JUC )包,在此包中增加了在并发编程中很常用的实用工具类,用于定义类似于线程的自定义子系统,包括线程池、异步 IO 和轻量级任务框架。提供可调的、灵活的线程池。还提供了设计用于多线程上下文中的 Collection 实现等.

volatile关键字

volatile:是一个关键字。volatile 的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略。尽量不用(与大多数场景冲突),根据经验它最适合使用的是一个线程写,多个线程读的场合,如果多个线程并发写操作,仍然需要使用锁或者线程安全的容器或者原子变量来代替。

保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性)

Java变量的读写

Java通过几种原子操作完成工作内存主内存的交互:

  1. lock:作用于主内存,把变量标识为线程独占状态。
  2. unlock:作用于主内存,解除独占状态。
  3. read:作用主内存,把一个变量的值从主内存传输到线程的工作内存。
  4. load:作用于工作内存,把read操作传过来的变量值放入工作内存的变量副本中。
  5. use:作用工作内存,把工作内存当中的一个变量值传给执行引擎。
  6. assign:作用工作内存,把一个从执行引擎接收到的值赋值给工作内存的变量。
  7. store:作用于工作内存的变量,把工作内存的一个变量的值传送到主内存中。
  8. write:作用于主内存的变量,把store操作传来的变量的值放入主内存的变量中。 

volatile如何保持内存可见性

volatile的特殊规则就是:

  • read、load、use动作必须连续出现
  • assign、store、write动作必须连续出现

所以,使用volatile变量能够保证:

  • 每次读取前必须先从主内存刷新最新的值。
  • 每次写入后必须立即同步回主内存当中

也就是说,volatile关键字修饰的变量看到的随时是自己的最新值。线程1中对变量v的最新修改,对线程2是可见的。

禁止进行指令重排序。(实现有序性)

volatile如何防止指令重排

在基于偏序关系Happens-Before内存模型中,指令重排技术大大提高了程序执行效率,但同时也引入了一些问题。

一个指令重排的问题——被部分初始化的对象

懒加载单例模式和竞态条件

一个懒加载单例模式实现如下:

class Singleton {
	private static Singleton instance;
	private Singleton(){}
	public static Singleton getInstance() {
		if ( instance == null ) { //这里存在竞态条件
			instance = new Singleton();
		}
		return instance;
	}
}

竞态条件会导致instance引用被多次赋值,使用户得到两个不同的单例。

DCL和被部分初始化的对象

为了解决这个问题,可以使用synchronized关键字将getInstance方法改为同步方法;但这样串行化的单例是不能忍的。所以我猿族前辈设计了DCL(Double Check Lock,双重检查锁)机制,使得大部分请求都不会进入阻塞代码块:

class Singleton {
	private static Singleton instance;
	private Singleton(){}
	public static Singleton getInstance() {
		if ( instance == null ) { //当instance不为null时,仍可能指向一个“被部分初始化的对象”
			synchronized (Singleton.class) {
				if ( instance == null ) {
					instance = new Singleton();
				}
			}
		}
		return instance;
	}
}

“看起来”非常完美:既减少了阻塞,又避免了竞态条件。不错,但实际上仍然存在一个问题——当instance不为null时,仍可能指向一个"被部分初始化的对象"

问题出在这行简单的赋值语句:

instance = new Singleton();

它并不是一个原子操作。事实上,它可以”抽象“为下面几条JVM指令:

memory = allocate();	//1:分配对象的内存空间
initInstance(memory);	//2:初始化对象
instance = memory;		//3:设置instance指向刚分配的内存地址

上面操作2依赖于操作1,但是操作3并不依赖于操作2,所以JVM可以以“优化”为目的对它们进行重排序,经过重排序后如下:

memory = allocate();	//1:分配对象的内存空间
instance = memory;		//3:设置instance指向刚分配的内存地址(此时对象还未初始化)
ctorInstance(memory);	//2:初始化对象

可以看到指令重排之后,操作 3 排在了操作 2 之前,即引用instance指向内存memory时,这段崭新的内存还没有初始化——即,引用instance指向了一个"被部分初始化的对象"。此时,如果另一个线程调用getInstance方法,由于instance已经指向了一块内存空间,从而if条件判为false,方法返回instance引用,用户得到了没有完成初始化的“半个”单例。
解决这个该问题,只需要将instance声明为volatile变量:

private static volatile Singleton instance;

也就是说,在只有DCL没有volatile的懒加载单例模式中,仍然存在着并发陷阱。我确实不会拿到两个不同的单例了,但我会拿到“半个”单例(未完成初始化)。

volatile如何防止指令重排

volatile关键字通过“内存屏障”来防止指令被重排序。

为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。然而,对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能,为此,Java内存模型采取保守策略。

下面是基于保守策略的JMM内存屏障插入策略:

  • 在每个volatile写操作的前面插入一个StoreStore屏障。
  • 在每个volatile写操作的后面插入一个StoreLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadStore屏障。

内存屏障的两个作用:阻止屏障两侧的指令重排序写的时候,强制把缓冲区/高速缓存中的数据写回主内存,并让缓冲中的数据失效;读的时候直接从主内存中读取

进阶

在一次回答上述问题时,忘记了解释一个很容易引起疑惑的问题:

如果存在这种重排序问题,那么synchronized代码块内部不是也可能出现相同的问题吗?

即这种情况:

class Singleton {
	...
		if ( instance == null ) { //可能发生不期望的指令重排
			synchronized (Singleton.class) {
				if ( instance == null ) {
					instance = new Singleton();
					System.out.println(instance.toString()); //程序顺序规则发挥效力的地方
				}
			}
		}
	...
}

难道调用instance.toString()方法时,instance也可能未完成初始化吗?

首先还请放宽心,synchronized代码块内部虽然会重排序,但不会在代码块的范围内导致线程安全问题

Happens-Before内存模型和程序顺序规则

程序顺序规则:如果程序中操作A在操作B之前,那么线程中操作A将在操作B之前执行。

前面说过,只有在Happens-Before内存模型中才会出现这样的指令重排序问题。Happens-Before内存模型维护了几种Happens-Before规则,程序顺序规则最基本的规则。程序顺序规则的目标对象是一段程序代码中的两个操作A、B,其保证此处的指令重排不会破坏操作A、B在代码中的先后顺序,但与不同代码甚至不同线程中的顺序无关

因此,在synchronized代码块内部,instance = new Singleton()仍然会指令重排序,但重排序之后的所有指令,仍然能够保证在instance.toString()之前执行。进一步的,单线程中,if ( instance == null )能保证在synchronized代码块之前执行;但多线程中,线程1中的if ( instance == null )却与线程2中的synchronized代码块之间没有偏序关系,因此线程2中synchronized代码块内部的指令重排对于线程1是不期望的,导致了此处的并发陷阱。

类似的Happens-Before规则还有volatile变量规则监视器锁规则等。程序猿可以借助(Piggyback)现有的Happens-Before规则来保持内存可见性和防止指令重排。

volatile 只能保证对单次读/写的原子性。

volatile关键字使变量的读、写具有了“原子性”。然而这种原子性仅限于变量(包括引用)的读和写,无法涵盖变量上的任何操作,即:

  • 基本类型的自增(如count++)等操作不是原子的。
  • 对象的任何非原子成员调用(包括成员变量成员方法)不是原子的。

如果希望上述操作也具有原子性,那么只能采取锁、原子变量更多的措施。

应用场景:

对变量的写操作不依赖于当前值。 (i++),例如 i++; 这个操作,它不是一个原子性操作,在实际执行时需要三步操作“读-改-写”:int temp = i;temp = temp + 1;i = temp;

该变量没有包含在具有其他变量的不变式中。

大多数编程情形都会与这两个条件的其中之一冲突,使得volatile变量不能像synchronized那样普遍用于实现线程安全。比如下面的例子中如果凑巧两个线程在同一时间使用不一致的值执行setlover和setupper的话,则会使范围处于不一致的状态,例如如果初始状态是(0,5)同一时间内,线程A调用setlover(4),并且线程B调用setUpper(3),显然这两个操作交叉存入的值不符合条件的,那么两个线程都会通过用于保护不变式的检查,使得最后的范围值是(4,3),最后获得一个无效的值。

如果volatile的修饰的是一个引用类型的对象变量,那么对象中定义的一些普通全局变量是否会受到volatile关键字的效果影响呢?

修饰基本类型:

package test;

import java.util.concurrent.TimeUnit;

public class volatileTest {
	//类变量
    final static int max = 5;
    static int init_value = 0;

    public static void main(String args[]) {
        //启动一个线程,当发现local_value与init_value不同时,则输出init_value被修改的值
        new Thread(() -> {
            int localValue = init_value;
            while (localValue < max) {
                if (init_value != localValue) {
                    System.out.printf("The init_value is update ot [%d]\n", init_value);
                    //对localValue进行重新赋值
                    localValue = init_value;
                }
            }
        }, "Reader").start();
        //启动updater线程,主要用于对init_value的修改,当local_value=5的时候退出生命周期
        new Thread(() -> {
            int localValue = init_value;
            while (localValue < max) {
                //修改init_value
                System.out.printf("The init_value will be changed to [%d]\n", ++localValue);
                init_value = localValue;
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "Updater").start();
        
        try
		{
			TimeUnit.SECONDS.sleep(12);
		} catch (InterruptedException e)
		{
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
        System.out.println("final main thread init_value is " + init_value);
    }
}

在上面的代码示例中,我们定义了两个类变量max、init_value,然后在主线程中分别启动一个Reader线程,一个Updater线程。Updater线程做的事情就是在值小于max的值时以每两毫秒的速度进行自增。而Reader线程则是在感知init_value值发生变化的情况下进行读取操作。

期望的效果是线程Updater更新init_value值之后,可以立刻被线程Reader感知到,从而进行输出显示。实际运行效果如下:

The init_value will be changed to [1]
The init_value will be changed to [2]
The init_value will be changed to [3]
The init_value will be changed to [4]
The init_value will be changed to [5]
final main thread init_value is 5

实际的运行效果是在Updater修改类变量init_value后,Reader线程并没有立马感知到变化,所以没有进行相应的显示输出。而原因就在于共享类变量init_value在被线程Updater拷贝到该线程的工作内存中后,Updater对变量init_value的修改都是在工作内存中进行的,完成操作后同步回主内存,但是Reader线程的的工作内存中对主内存的改变并不可见。

为了解决线程间对类变量init_value的可见性问题,我们将类变量init_value用volatile关键字进行下修饰,如下:

static volatile int init_value = 0;
运行结果:
The init_value will be changed to [1]
The init_value is update ot [1]
The init_value will be changed to [2]
The init_value is update ot [2]
The init_value will be changed to [3]
The init_value is update ot [3]
The init_value will be changed to [4]
The init_value is update ot [4]
The init_value will be changed to [5]
The init_value is update ot [5]
final main thread init_value is 5

此时线程Updater对类变量init_value的修改,立马就能被Reader线程感知到了,这就是volatile关键字的效果,可以让共享变量在线程间实现可见,原因就在于在JVM的语义层面要求被volatile修饰的共享变量,在工作内存中的修改要立刻同步回主内存,并且读取也需要每次都重新从主内存中刷新一份到工作内存中后才可以操作。

修饰引用类型:

package test;

import java.util.concurrent.TimeUnit;

public class VolatileEntityTest {

	//使用volatile修饰共享资源
	//我们将之前代码中的类变量init_value放到实体类VolatileEntity中,并将其设计为一个实例变量,
	//为了便于理解,我们将实体类VolatileEntity设计为单例模式,确保两个线程操作的是同一个共享堆内存对象。如下:
    private static VolatileEntity volatileEntity = VolatileEntity.getInstance();
    public static void main(String args[]) throws InterruptedException {
        //启动一个线程,当发现local_value与init_value不同时,则输出init_value被修改的值
        new Thread(() -> {
            int localValue = volatileEntity.person.init_value;
            while (localValue < VolatileEntity.max) {
                if (volatileEntity.person.init_value != localValue) {
                    System.out.printf("The init_value is update ot [%d]\n", volatileEntity.person.init_value);
                    //对localValue进行重新赋值
                    localValue = volatileEntity.person.init_value;
                }
            }
        }, "Reader").start();

        //启动updater线程,主要用于对init_value的修改,当local_value=5的时候退出生命周期
        new Thread(() -> {
            int localValue = volatileEntity.person.init_value;
            while (localValue < VolatileEntity.max) {
                //修改init_value
                System.out.printf("The init_value will be changed to [%d]\n", ++localValue);
                volatileEntity.person.init_value = localValue;
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "Updater").start();
        
        try
		{
			TimeUnit.SECONDS.sleep(12);
			System.out.println("final main thread init_value is " + volatileEntity.person.init_value);
		} catch (InterruptedException e)
		{
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
    }
}


class VolatileEntity {
    //使用volatile修饰共享资源i
    //类变量
    final static int max = 5;
    Person person = new Person();
    public static int getMax() {
        return max;
    }

    private static class VolatileEntityHolder {
        private static VolatileEntity instance = new VolatileEntity();
    }
    public static VolatileEntity getInstance() {
        return VolatileEntityHolder.instance;
    }
}

class Person
{
	int init_value = 0;
}

结果如下:
The init_value will be changed to [1]
The init_value will be changed to [2]
The init_value will be changed to [3]
The init_value will be changed to [4]
The init_value will be changed to [5]
final main thread init_value is 5

与未被volatile修饰的int类型的类变量效果一样,线程Updater对VolatileEntity对象中init_value变量的操作也不能立马被线程Reader可见。如果此时我们不在VolatileEntity类中单独用volatile关键字修饰init_value变量,而是直接用volatile关键字修饰VolatileEntity对象,效果会如何呢?

private static volatile VolatileEntity volatileEntity = VolatileEntity.getInstance();

The init_value will be changed to [1]
The init_value is update ot [1]
The init_value will be changed to [2]
The init_value is update ot [2]
The init_value will be changed to [3]
The init_value is update ot [3]
The init_value will be changed to [4]
The init_value is update ot [4]
The init_value will be changed to [5]
The init_value is update ot [5]
final main thread init_value is 5

从实际的运行效果上看,虽然我们没有直接用volatile关键字修饰对象中的类变量init_value,而是修改了对象的引用,但是我们看到对象中的普通实例变量仍然实行了线程间的可见性,也就是说间接也相当于被volatile关键字修饰了。所以,在这里问题也就基本上有了答案,那就是:“被volatile关键字修饰的引用类型对象,其对象中携带的类变量也相当于被volatile关键字修饰了”

乐观锁与悲观锁:两者并不是特指某个锁,而是在并发情况下保证数据完整性的不同策略。悲观锁指的就是我们平常使用的加锁机制,它假设我们总是处于最坏的情况下,如果不加锁数据完整性就会被破坏。而乐观锁指是一种基于冲突检测的方法,检测到冲突时操作就会失败。synchronizedReentrantLock等独占锁就是悲观锁思想的实现,java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

公平锁:表示线程获取锁的顺序是按照加锁的顺序来分配的,及先来先得,先进先出的顺序。
非公平锁:表示获取锁的抢占机制,是随机获取锁的,和公平锁不一样的就是先来的不一定能拿到锁。ReentrantLock等锁在构建的时候可以传入boolean 参数来构建两种锁。

CAS算法

CAS(Compare And Swap)是一种常见的“乐观锁,大部分的CPU都有对应的汇编指令,在并发数不是特别高的情况下,使用CAS的乐观锁在性能上要优于使用加锁方式的悲观锁,因为大部分情况下经过数次轮询后CAS操作都可以成功,而使用加锁机制则会造成线程的阻塞与调度,相对而言更耗时。Unsafe,是CAS的核心类,java不能直接访问操作系统底层,而是通过本地方法来访问。Unsafe类提供了硬件级别的原子操作。下面模拟了CAS的例子。

CAS 是一种无锁的非阻塞算法的实现。

CAS 包含了 3 个操作数:需要读写的内存值 V、进行比较的值 A、拟写入的新值 B

当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值 B 来更新 V 的值,否则不会执行任何操作。

java.util.concurrent.atomic包下的AtomicInteger、AtomicReference等类,它们提供了基于CAS的读写操作和并发环境下的内存可见性。

AtomicInteger:保证了原子性。但是性能下降。核心方法:boolean compareAndSet(expectedValue, updateValue),AtomicBoolean、AtomicInteger、AtomicLong 和 AtomicReference 的实例各自提供对相应类型单个变量的访问和更新。每个类也为该类型提供适当的实用工具方法。AtomicIntegerArray、AtomicLongArray 和 AtomicReferenceArray 这些类在为其数组元素提供原子更新。

CountDownLatch 计数锁 闭锁

CountDownLatch   不可序列化,一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待,用给定的计数 初始化 CountDownLatch,countDown() 每被调用一次,这一数量就减一所以在当前计数到达零之前,await 方法会一直受阻塞。之后,会释放所有等待的线程 ,await 的所有后续调用都将立即返回。这种现象只出现一次——计数无法被重置。闭锁可以延迟线程的进度直到其到达终止状态,闭锁可以用来确保某些活动直到其他活动都完成才继续执行,可以确保某个计算在其需要的所有资源都被初始化之后才继续执行;也可以确保某个服务在其依赖的所有其他服务都已经启动之后才启动;也可以等待直到某个操作所有参与者都准备就绪再继续执行。

ConcurrentHashMap 

ConcurrentHashMap 内部1.8前采用“锁分段”机制替代 Hashtable 的独占锁,1.8后运用CAS + synchronized。进而提高性能。详解见https://blog.csdn.net/weixin_39407066/article/details/88697827此篇博客。
此包还提供了设计用于多线程Collection 实现:ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、CopyOnWriteArrayList 和CopyOnWriteArraySet。当期望许多线程访问一个给定 collection 时,ConcurrentHashMap 通常优于同步的 HashMap,ConcurrentSkipListMap 通常优于同步的 TreeMap。当期望的读数和遍历远远大于列表的更新数时,CopyOnWriteArrayList 优于同步的 ArrayList。

ReadWriteLock 读写锁
ReadWriteLock 维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有 writer,读取锁可以由多个 reader 线程同时保持。写入锁是独占的。ReadWriteLock 读取操作通常不会改变共享资源,但执行写入操作时,必须独占方式来获取锁。 ReadWriteLock 能提供比独占锁更高的并发性。而对于只读的数据结构,其中包含的不变性可以完全不需要考虑加锁操作.写写和读写是互斥的, 读读不需要互斥。

线程池

详情见https://blog.csdn.net/weixin_39407066/article/details/88871585此篇文章。

Fork/Join 框架

从JDK1.7开始,Java提供Fork/Join框架用于并行执行任务,它的思想就是讲一个大任务分割成若干小任务,最终汇总每个小任务的结果得到这个大任务的结果。主要分两步:任务切分和结果合并。它的模型大致是这样的:线程池中的每个线程都有自己的工作队列(PS:这一点和ThreadPoolExecutor不同,ThreadPoolExecutor是所有线程公用一个工作队列,所有线程都从这个工作队列中取任务,所以当一个大任务分配成小任务由多个线程执行肯定比有一个线程执行效率高),当自己队列中的任务都完成以后,会从其它线程的工作队列中偷一个任务执行,这样可以充分利用资源,而窃取的时候为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。

Fork/Join 框架与线程池的区别

采用 “工作窃取”模式(work-stealing):当执行新的任务时它可以将其拆分分成更小的任务执行,并将小任务加到线程队列中,线程和队列一一对应,率先执行完毕的线程去其他线程的队列里窃取一个任务来执行。
相对于一般的线程池实现,fork/join框架的优势体现在对其中包含的任务的处理方式上.在一般的线程池中,如果一个线程正在执行的任务由于某些原因无法继续运行,那么该线程会处于等待状态。而在fork/join框架实现中,如果某个子问题由于等待另外一个子问题的完成而无法继续运行。那么处理该子问题的线程会主动寻找其他尚未运行的子问题来执行.这种方式减少了线程的等待时间,提高了性能。sync mode 默认为false, 当设置为true时,只有当任务未进行实际的递归分解时,它才执行异步任务,然后才实际查询asyncMode标志,在sync mode模式下,从未加入的分叉任务以LIFO(先进先出)顺序执行,而默认为false时是以LIFO(后进先出)顺序处理此类任务。什么时候使用哪种模式?每当选择ForkJoinPool线程池以利用其工作窃取属性而不是进行递归fork / join任务处理时,异步模式可能是更自然的选择,因为任务按提交顺序执行。有时会说异步事件驱动的框架(例如CompletableFuture)可以更效率。例如,在构造复杂的CompletableFuture回调链时,异步模式下的自定义ForkJoinPool执行程序的性能可能比默认执行程序稍好。 

ForkJoinTask:ForkJoinTask代表运行在ForkJoinPool中的任务。

主要方法:

  • fork()    在当前线程运行的线程池中安排一个异步执行。简单的理解就是再创建一个子任务。
  • join()    当任务完成的时候返回计算结果。
  • invoke()    开始执行任务,如果必要,等待计算完成。

子类:

  • RecursiveAction    一个递归无结果的ForkJoinTask(没有返回值)
  • RecursiveTask    一个递归有结果的ForkJoinTask(有返回值)

ForkJoinWorkerThread:ForkJoinWorkerThread代表ForkJoinPool线程池中的一个执行任务的线程.

WorkQueue是一个ForkJoinPool中的内部类,它是线程池中线程的工作队列的一个封装,支持任务窃取.

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值