高并发编程(三)如何编写线程安全的代码

                            高并发编程(三)如何编写线程安全的代码

 

  • 线程安全类

线程安全性

定义:当多个线程访问某个类的时候,不管运行环境采用何种调度方式或者这些进程如何交替执行,并且在主调代码中不需要采用额外的同步或者是协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的

原子性:提供了互斥访问,同一时刻只能有一个线程来对它进行操作。

    Atomic包

    Atomic:CAS、Unsafe.compareAndSwapInt

    AtmomicLong、LongAdder

    AtomicReference、AtomicReferenceFieldUpdater

    AtomicStampReference:CAS的ABA问题

   

    线程不安全的累加案例

package com.lb.api.thread;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

public class ThreadTest {
	static int count=0;
	public static void main(String[] args) throws InterruptedException {
		  ExecutorService service = Executors.newCachedThreadPool();
		
	        for(int i=0;i<10;i++){
	            Runnable runnable = new Runnable(){
	                    public void run(){
	                    	count++;
	                    	System.out.println("count="+count);
	                }
	            };
	            service.execute(runnable);            
	        }
	}

}

结果显示:

count=3
count=3
count=3
count=4
count=5
count=6
count=7
count=8
count=9
count=10

Atomic类实现线程安全

package com.lb.api.thread;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;

public class ThreadTest {
	 static AtomicInteger count=new AtomicInteger(0);
	public static void main(String[] args) throws InterruptedException {
		  ExecutorService service = Executors.newCachedThreadPool();
		
	        for(int i=0;i<10;i++){
	            Runnable runnable = new Runnable(){
	                    public void run(){
	                    	System.out.println("count="+count.incrementAndGet());
	                }
	            };
	            service.execute(runnable);            
	        }
	}

}

结果显示:

count=2
count=1
count=3
count=4
count=5
count=6
count=7
count=8
count=9
count=10

最终的输出结果为,可见这个程序是线程安全的。如果把AtomicInteger换成变量i的话,那最终结果就不确定了。

打开AtomicInteger的源码可以看到:

// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private volatile int value;

volatile关键字用来保证内存的可见性(但不能保证线程安全性),线程读的时候直接去主内存读,写操作完成的时候立即把数据刷新到主内存当中。

CAS简要

/**
     * Atomically sets the value to the given updated value
     * if the current value {@code ==} the expected value.
     *
     * @param expect the expected value
     * @param update the new value
     * @return {@code true} if successful. False return indicates that
     * the actual value was not equal to the expected value.
     */
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

从注释就可以看出:当线程写数据的时候,先对内存中要操作的数据保留一份旧值,真正写的时候,比较当前的值是否和旧值相同,如果相同,则进行写操作。如果不同,说明在此期间值已经被修改过,则重新尝试。
compareAndSet使用Unsafe调用native本地方法CAS(CompareAndSet)递增数值。

CAS利用CPU调用底层指令实现。

 Atomic包除了可以实现基本数据类型的原子操作,还能实现对象的原子操作。

 AtomicReference对象的原子操作类

实现 原子操作

使用场景:

一个线程使用student对象,另一个线程负责定时读表,更新这个对象。那么就可以用AtomicReference。

赋值操作不是线程安全的。若想不用锁来实现,可以用AtomicReference<V>实现对象引用的原子更新。

 

 

        虽然在大多数的情况下Atomic包下的原子类操作已经够但是在极度竞争的条件下,即高并发严重的情况下,Atomic的性能表现不是非常理想,结合它的原理,在极度竞争的条件下,每个线程都在进行比较等待的过程,这个现象被称为过度自旋。这时程序的性能急剧下降。那么这时候我们应该怎么办?

       IntegerAddr类实现线程安全

       

package com.lb.api.thread;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.LongAdder;

public class ThreadTest {
	 static LongAdder count=new LongAdder();
	public static void main(String[] args) throws InterruptedException {
		  ExecutorService service = Executors.newCachedThreadPool();
		
	        for(int i=0;i<10;i++){
	            Runnable runnable = new Runnable(){
	                    public void run(){
//	                    	synchronized (this) {
	                    		count.increment();
//		                    	System.out.println("count="+count.intValue());
//							}
	                    	
	                }
	            };
	            service.execute(runnable);   
	        }
	        TimeUnit.SECONDS.sleep(1);
	        System.out.println("count="+count.intValue());
	}

}

 结果:count=10

原理参考我的文章LongAdder原理解析

 

  Synchronized实现线程安全

 

package com.lb.api.thread;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.LongAdder;

public class ThreadTest {
	static int count=0;
	public static void main(String[] args) throws InterruptedException {
		  ExecutorService service = Executors.newCachedThreadPool();
		
	        for(int i=0;i<10;i++){
	            Runnable runnable = new Runnable(){
	                    public void run(){
	                    	synchronized (this) {
	                    		count++;
		                    	System.out.println("count="+count);
							}
	                    	
	                }
	            };
	            service.execute(runnable);   
	        }
	        System.out.println("count="+count);
	}

}


  结果:count=10
 

 ReentrantLock锁实现线程安全

  

package com.lb.api.thread;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.LongAdder;
import java.util.concurrent.locks.ReentrantLock;

public class ThreadTest {
	private static ReentrantLock lock=new ReentrantLock();
	static int count=0;
	public static void main(String[] args) throws InterruptedException {
		  ExecutorService service = Executors.newCachedThreadPool();
		
	        for(int i=0;i<10;i++){
	            Runnable runnable = new Runnable(){
	            	
	                    public void run(){
	                    	lock.lock();
	                    		count++;
	                    		lock.unlock();
							}
	                    	
	            };
	            service.execute(runnable);   
	        }
	        service.shutdown();
	        service.awaitTermination(1, TimeUnit.HOURS);
	        System.out.println("count="+count);
	}

}

   结果:count=10

  

对比

ReentrantLock与synchronized简单对比
ReentrantLock是JDK1.5之后引入的,synchronized作为关键字在ReentrantLock引入后进行的大量修改性能不断提升;

1.可重入性
ReentrantLock和synchronized都具有可重入性,写代码synchronized更简单,ReentrantLock需要将lock()和unlock()进行一一对应否则有死锁的风险;

2.锁的实现方式
Synchronized作为Java关键字是依赖于JVM实现的,而ReenTrantLock是JDK实现的,有什么区别,说白了就类似于操作系统来控制实现和用户自己敲代码实现的区别。前者的实现是比较难见到的,后者有直接的源码可供阅读。

3.公平性
ReentrantLock提供了公平锁和非公平锁两种API,开发人员完全可以根据应用场景选择锁的公平性;
synchronized是作为Java关键字是依赖于JVM实现,Java团队应该是优先考虑性能问题,因此synchronized是非公平锁。
 

 Atomic:竞争激烈时候可以维持常态,比Lock性能好;只能同步一个值。

 

 线程安全的类:

 

ArrayList->Vector、Stack

    HashMap->HashTable(key,value不能为null)

    Collections.synchronizedXXX(List、Set、Map)

 

线程安全(并发容器    J.U.C)

ArrayList->CopyOnWriteArrayList(适合读多写少的场景、读写分离、读不加锁、写加锁)

HashSet、TreeSet->CopyOnWriteArraySet、ConcurrentSkipListSet

HashMap、TreeMap->ConcurrentHashMap、ConcurrentSkipListMap
 

 

  • CountDownLatch

CountDownLatch是一个同步工具类,用来协调多个线程之间的同步,或者说起到线程之间的通信(而不是用作互斥的作用)。

CountDownLatch能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。使用一个计数器进行实现。计数器初始值为线程的数量。当每一个线程完成自己任务后,计数器的值就会减一。当计数器的值为0时,表示所有的线程都已经完成了任务,然后在CountDownLatch上等待的线程就可以恢复执行任务。

用法:

CountDownLatch典型用法1:某一线程在开始运行前等待n个线程执行完毕。将CountDownLatch的计数器初始化为n new CountDownLatch(n) ,每当一个任务线程执行完毕,就将计数器减1 countdownlatch.countDown(),当计数器的值变为0时,在CountDownLatch上 await() 的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。

CountDownLatch典型用法2:实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的CountDownLatch(1),将其计数器初始化为1,多个线程在开始执行任务前首先 coundownlatch.await(),当主线程调用 countDown() 时,计数器变为0,多个线程同时被唤醒
 

package com.lb.api.thread;

import java.util.concurrent.CountDownLatch;

public class ThreadTest {
	
	public static void main(String[] args) throws InterruptedException {
		CountDownLatch c=new CountDownLatch(2);
		new Thread(()->{
			try {
				  Thread.sleep(10000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			System.out.println("a");
			c.countDown();
		}).start();
		new Thread(()->{
			System.out.println("b");
			c.countDown();
		}).start();
		
		c.await();
		System.out.println("c");
//		  Thread.sleep(30000);
	}

}

  结果输出  a  b  c

CountDownLatch底层原理

CountDownLatch通过AQS(AbstractQueuedSynchronizer)里面的共享锁来实现的。
ReentrantLock也是使用AQS

 

AbstractQuenedSynchronizer-AQS(重中之重)

 

    1)使用Node实现FIFO队列、可以用于构建锁或者其他同步装置的基础框架。

    2)利用了一个int类型表示状态。

    3)使用方法是继承。

    4)子类通过继承实现它的方法管理其状态{acquire和release}的方法操纵状态。

    5)可以同时实现排它锁和共享锁(独占、共享)

 

AQS实现的大致思路:

    AQS内部维护了一个CLH队列来管理锁,线程会首先尝试获取锁,如果失败,会将当前线程以及等待状态的信息包装成一个Node节点加入到同步队列,接着不断循环尝试获取锁,它的条件是当前节点为head的直接后继,如果失败就会阻塞自己,直到自己被唤醒,当持有锁的线程释放锁的时候会唤醒队列中的后继线程。

 

AQS同步组件

 

    CountDownLatch(闭锁,通过一计数来保证线程是否需要一直阻塞)

    Semaphore(可以控制线程并发数目)

    CyclicBarrier

    ReentrantLock(重点)

    Condition

    FutureTask

 
Semaphore

 

Semaphore是计数信号量。Semaphore管理一系列许可证。每个acquire方法阻塞,直到有一个许可证可以获得然后拿走一个许可证;每个release方法增加一个许可证,这可能会释放一个阻塞的acquire方法。然而,其实并没有实际的许可证这个对象,Semaphore只是维持了一个可获得许可证的数量。 

Semaphore经常用于限制获取某种资源的线程数量。
 

package com.lb.api.thread;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

public class ThreadTest {
	
	public static void main(String[] args) throws InterruptedException {
		  ExecutorService service = Executors.newCachedThreadPool();
	        final  Semaphore sp = new Semaphore(3);//创建Semaphore信号量,初始化许可大小为3
	        for(int i=0;i<10;i++){
	            try {
	                Thread.sleep(100);
	            } catch (InterruptedException e2) {
	                e2.printStackTrace();
	            }
	            Runnable runnable = new Runnable(){
	                    public void run(){
	                    try {
	                        sp.acquire();//请求获得许可,如果有可获得的许可则继续往下执行,许可数减1。否则进入阻塞状态
	                    } catch (InterruptedException e1) {
	                        e1.printStackTrace();
	                    }
	                    System.out.println("线程" + Thread.currentThread().getName() + 
	                            "进入,当前已有" + (3-sp.availablePermits()) + "个并发");
	                    try {
	                        Thread.sleep((long)(Math.random()*10000));
	                    } catch (InterruptedException e) {
	                        e.printStackTrace();
	                    }
	                    System.out.println("线程" + Thread.currentThread().getName() + 
	                            "即将离开");                    
	                    sp.release();//释放许可,许可数加1
	                    //下面代码有时候执行不准确,因为其没有和上面的代码合成原子单元
	                    System.out.println("线程" + Thread.currentThread().getName() + 
	                            "已离开,当前已有" + (3-sp.availablePermits()) + "个并发");                    
	                }
	            };
	            service.execute(runnable);            
	        }
	}

}

 

结果打印:

线程pool-1-thread-1进入,当前已有1个并发
线程pool-1-thread-2进入,当前已有2个并发
线程pool-1-thread-3进入,当前已有3个并发
线程pool-1-thread-2即将离开
线程pool-1-thread-4进入,当前已有3个并发
线程pool-1-thread-2已离开,当前已有3个并发
线程pool-1-thread-3即将离开
线程pool-1-thread-3已离开,当前已有3个并发
线程pool-1-thread-2进入,当前已有3个并发
线程pool-1-thread-1即将离开
线程pool-1-thread-5进入,当前已有3个并发
线程pool-1-thread-1已离开,当前已有3个并发
线程pool-1-thread-4即将离开
线程pool-1-thread-4已离开,当前已有2个并发
线程pool-1-thread-6进入,当前已有3个并发
线程pool-1-thread-5即将离开
线程pool-1-thread-5已离开,当前已有2个并发
线程pool-1-thread-7进入,当前已有3个并发
线程pool-1-thread-2即将离开
线程pool-1-thread-8进入,当前已有3个并发
线程pool-1-thread-2已离开,当前已有3个并发
线程pool-1-thread-7即将离开
线程pool-1-thread-7已离开,当前已有2个并发
线程pool-1-thread-9进入,当前已有3个并发
线程pool-1-thread-6即将离开
线程pool-1-thread-6已离开,当前已有2个并发
线程pool-1-thread-8即将离开
线程pool-1-thread-8已离开,当前已有1个并发
线程pool-1-thread-9即将离开
线程pool-1-thread-9已离开,当前已有0个并发

 CyclicBarrier

 

类似于CountDownLatch,不过可以循环。

CountDownLatch描述的是一个或n个线程需要等待其他线程完成某个操作之后才能往下执行;CyclicBarrier描述的是多个线程之间等待直到所有线程都满足条件后才可执行。

 

 以上所有关于线程安全的知识我只是做到了抛砖引玉的讲解,具体的还要读者自己去好好消化专研。

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值