Java7技术系列:Java并发

Java7技术系列:try-with-resource
Java7技术系列:int与二进制的转换优化
Java7技术系列:MultiCatchException
Java7技术系列:NIO.2异步IO
Java7技术系列:DI依赖注入
Java7技术系列:Queue
Java7技术系列:Java并发

1 传统并发(Java5之前)

1.1 同步与锁

synchronized既可以用在代码块上也可以用在方法上。它表明在执行整个代码块或方法之前线程必须取得合适的锁。
对于方法而言,这意味着要取得对象实例锁(对于静态方法而言则是类锁)。
对于代码块,则应该指明要取得哪个对象的锁。

java中的同步和锁相关知识:

  • 只能锁定对象,不能锁定原始类型。

  • 被锁定的对象数组中的单个对象不会被锁定。

  • 同步方法可以视同为包含整个方法的同步代码块(但要注意它们的二进制码表示是不同的),就是:

public synchronized void test() {}
相当于
public void test() {
   synchronized(this) {}
}
  • 静态同步方法会锁定它的Class对象,因为没有实例对象可以锁定。

  • 如果要锁定一个类对象,请慎重考虑是用显式锁定,还是用getClass(),两种方式对子类的影响不同。

  • 内部类的同步是独立于外部类的。

  • synchronized并不是方法签名的组成部分,所以不能出现在接口的方法声明中。

  • 非同步的方法不查看或关心任何锁的状态,而且在同步方法运行时它们仍能继续运行。

  • java的线程锁是可重入的。也就是说持有锁的线程在遇到同一个锁的同步点(比如一个同步方法调用同一个类的另一个同步方法)时是可以继续的。

1.2 被synchronized同步的是什么?

被同步的是在不同线程中表示被锁定对象的内存块。也就是说,在synchronized代码块(或方法)执行完后,对被锁定对象所做的任何修改全部都会在线程锁释放之前刷回到主内存中。另外,当进入一个同步的代码块,得到线程锁之后,对被锁定对象的任何修改都是从主内存中读出来的,所以在锁定区域代码开始执行之前,持有锁的线程就和锁定对象主内存中的试图同步了。

2 关键字volatile

在理解轻量级同步volatile之前,需要了解一下java的内存模型JMM(Java Memory Model):

java虚拟机有自己的内存模型JMM,JMM可以屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致内存访问。

JMM决定一个线程对共享变量的写入何时对另一个线程”可见“,JMM定义了线程和主内存之间的抽象关系:
共享变量存储在主内存中(Main Memory),每个线程都有一个私有的本地内存(Local Memory),本地内存保存了被该线程使用到的主内存副本拷贝,线程对该变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。

简单理解:
每个线程在运行时先从主内存中读取共享变量,也就是拷贝一个副本,在私有的工作内存中对副本变量操作后,结束线程前将副本的值写入主内存中更新主内存的共享变量,而A线程对共享变量的操作在刷回主内存前,对于B线程而言是”不可见“的

volatile 是一种简单的对象域同步处理办法,包括原始类型。一个volatile域需遵循如下规则:

  • 线程所见的值在使用之前总会从主内存中再读出来

  • 线程锁写的值总会在指定完成之前被刷回到主内存中可以把围绕该域的操作堪称是一个小小的同步块,但付出的代价是每次访问都要额外刷一次内存。

简单理解:
1、在多线程操作时,A线程在主内存中读取了共享变量,该变量若声明为volatile,JMM会把该线程对应的本地内存中的变量强制刷新到主内存中。
2、这个写操作会导致其他线程的缓存无效。即其他线程同样读取了同一共享变量,A线程对先将对共享变量的修改刷回主内存中,B线程对该共享变量的修改就是无效的,只认为A线程操作有效。

volatile变量是线程安全的,但只有写入时不依赖当前状态(读取的状态)的变量才应该声明为volatile变量。

注意:

  • volatile变量不会引入线程锁,所以使用volatile变量不可能发生死锁。

  • volatile只支持对于单个的共享变量的读/写具有原子性操作,不支持复合操作(比如num++)。

volatile并不能完全替代synchronized,在很多场景下,volatile并不能胜任。

举例:复合类操作导致volatile数据显示不正确

public static volatile int num = 0;
static CountDownLatch cdl = new CountDownLatch(30); // 传递30个线程数量
         
for (int i = 0; i < 30; i++) {
	new Thread() {
		public void run() {
                      for (int j = 0; j < 10000; j++) {
                          num++;
                      }
		      cdl.countDown(); // 每个线程循环完成调用countDown()减1
                }
        }.start();
}
cdl.await();  // 30个线程各自的循环执行完成,countDown()最后为0,调用await()结束等待
System.out.println(num);

上面的例子创建了30个线程,如果同步正确的话num的值应该为30_0000,但最终结果有可能小于30_0000。
原因:num++不是原子性的操作,而是个复合操作,该操作分为三步:
①读取
②加一
③赋值
所以在多线程环境下,有可能线程A将num读取到本地内存中,此时其他线程可能已经将num增大了很多,线程A依然对过期的num进行自加,重新写到主内存中,最终导致了num的结果不合预期,而是小于30_0000(这里以A线程为主,就会导致其他线程的写入无效)。

该复合操作问题可以通过java.util.concurrent.atomic原子类解决

原子类:java.util.concurrent.atomic

public static AtomicInteger num = new AtomicInteger();
static CountDownLatch cdl = new CountDownLatch(30);
         
for (int i = 0; i < 30; i++) {
	new Thread(){
		public void run() {
			for (int j = 0; j < 10000; j++) {
				num.incrementAndGet();
			}
			cdl.countDown();
		}
	}.start();
}
cdl.await();
System.out.println(num);

在编写这些实现时利用了现代处理器的特性,所以如果能从硬件和操作系统上得到适当的支持,它们可以是非阻塞(无需线程锁)的,
而大多数现代系统都能提供这种支持。
常见的用法是实现序列号机制,在AtomicInteger或AtomicLong上用原子操作getAndIncrement()方法,每次调用时肯定能返回一个唯一
并且完全增长的数值。

注意:原子类不是从有相似名称的类继承来的,所以AtomicBoolean不能当Boolean用,AtomicInteger也不是Integer,虽然它确实扩展了NUmber

3 CountDownLatch锁存器

CountDownLatch是一种简单的同步模式,这种模式允许线程在通过同步屏障之前做些少量的准备工作。
它有两个控制锁存器的方法:countDown()和await()。前者对计数器减1,而后者让调用线程在计数器到0之前一直等待。
如果计数器已经为0或更小,则它什么也不做。

【简单理解:CountDownLatch就是一个能够在多线程情况下控制线程执行顺序的类】

举例:同一进程内的一组更新处理线程至少必须有一半线程正确初始化(假定更新处理线程需要占用一定时间)之后,才能开始接受系统发送给它们中的任何一个线程的更新。

 public static class ProcessingThread extends Thread {
     private final String ident;
     private final CountDownLatch latch;

     public ProcessingThread(String ident, CountDownLatch latch) {
         this.ident = ident;
         this.latch = latch;
     }

     public String getIdentifier() {
         return identifier;
     }

     public void initialize() {
         latch.countDown();
     }

     @Override
     public void run() {
         initialize();
     }
 }

 final int quorum = 1 + (int)(MAX_THREADS / 2); // 半数处理线程
 final CountDownLatch cdl = new CountDownLatch(quorum); // 传递一个数值用于计数
 final Set<ProgressThread> nodes = new HastHse<>();

 try {
     for (int i = 0; i < MAX_THREADS; i++) {
         ProgressThread thread = new ProgressThread("localhost:" + (9000 + i), cdl);
         nodes.add(thread);
         thread.start(); // 每次开启线程时调用countDown()减1,剩下的一半循环线程调用countDown()什么也不做
     }
      // 调用await()阻塞,目前给予的是数值是30,在初始化线程达到15个时,await()不再阻塞往下执行,打印出结果
     cdl.await();
     System.out.println("half thread is initialized");	
 } catch (InterruptedException e) {
     e.printStackTrace();
 }

另一个简单例子:

final CountDownLatch cdl = new CountDownLatch(2);

new Thread(new Runnable() {
@Override
public void run() {
   System.out.println("线程1" + Thread.currentThread().getName() + "正在执行"); // ②
   try {
       Thread.sleep(3000);
   } catch (InterruptedException e) {
       e.printStackTrace();
   }
   System.out.println("线程1" + Thread.currentThread().getName() + "执行结束"); // ④
   cdl.countDown();
}
}).start();

new Thread(new Runnable() {
@Override
public void run() {
   System.out.println("线程2" + Thread.currentThread().getName() + "正在执行"); // ③
   try {
       Thread.sleep(3000);
   } catch (InterruptedException e) {
       e.printStackTrace();
   }
   System.out.println("线程2" + Thread.currentThread().getName() + "执行结束"); // ⑤
   cdl.countDown();
}
}).start();

System.out.println("线程正在执行"); // ①
try {
cdl.await(); 
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("所有线程执行结束"); // ⑥

打印结果:
线程正在执行
线程1Thread-0正在执行
线程2Thread-1正在执行
线程1Thread-0执行结束
线程2Thread-1执行结束
所有线程执行结束

4 CopyOnWriteArrayList

CopyOnWriteArrayList是标准ArrayList的替代品。CopyOnWriteArrayList通过增加写时复制(copy-on-write)语义来实现线程安全性,也就是说修改列表的任何操作都会创建一个列表底层数组的新复本。

简单理解:
CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,
而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。
这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

CopyOnWriteArrayList的实现原理:

  • 在添加调用add()方法时是需要加锁的,否则多线程写的时候会copy出N个副本。

  • 在读的时候不需要加锁,如果读的时候有多个线程正在向CopyOnWriteArrayList添加数据,读还是会读到旧的列表数据,因为写的时候不会锁住旧的CopyOnWriteArrayList。

CopyOnWriteArrayList的应用场景:

用于读多写少的并发场景。因为在写时会复制副本,所以能避免多线程情况下发ConcurrentModificationException。

public class MicroBlogTimeline {
     private final CopyOnWriteArrayList<Update> updates;
     private final ReentrantLock lock;
     private final String name;
     private Iterator<Update> it;

     public void addUpdate(Update update) {
         updates.add(update);
     }

     public void prep() {
         it = updates.iterator();
     }

     public void printTimeline() {
         lock.lock();
         try {
             if (it != null) {
                 System.out.println(name + "");
                 while (it.hasNext()) {
                     Update u = it.next();
                     System.out.print(u);
                 }
             }
         } finally {
             lock.unlock();
         }
     }
 }

 // 使用锁存器严格限制线程执行顺序
 final CountDownLatch firstLatch = new CountDownLatch(1);
 final CountDownLatch secondLatch = new CountDownLatch(1);
 final Update.Builder ub = new Update.Builder();

 final List<Update> list = new ArrayList<>();
 list.add(ub.author(new Author("Ben")).updateText("I like pie").build());
 list.add(ub.author(new Author("Charles").updateText("I like ham on rye").build());

 ReentrantLock lock = new ReentrantLock();
 final MicroBlogTimeline tl1 = new MicroBlogTimeline("TL1", list, lock);
 final MicroBlogTimeline tl2 = new MicroBlogTimeline("TL2", list, lock);

 Thread t1 = new Thread() {
     public void run() {
         list.add(ub.author(new Author("Jeffery").updateText("I like a lot of things").build());
         tl1.prep();
         firstLatch.countDown(); // 如果先执行t1线程,则添加完Jeffery后停止阻塞让其他t2线程执行
         try {
             firstLatch.await();
         } catch (InterruptException e) {
             e.printStackTrance();
         }
         tl1.preTimeline();
     }
 };

 Thread t2 = new Thread() {
     public void run() {
         try {
             firstLatch.await();
             list.add(ub.author(new Author("Gavin").updateText("I like otters").build()));
             tl2.prep();
             secondLatch.countDown();
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         tl2.printTimeline();
     }
 };

 t1.start();
 t2.start();

 打印结果:
 TL2:
 Update [author=Author [name=Ben], updateText=I like pie],
 Update [author=Author [name=Charles], updateText=I like ham on rye],
 Update [author=Author [name=Jeffery], updateText=I like a lot of things],
 Update [author=Author [name=Gavin], updateText=I like otters]
 
 TL1:
 Update [author=Author [name=Ben], updateText=I like pie],
 Update [author=Author [name=Charles], updateText=I like ham on rye],
 Update [author=Author [name=Jeffery], updateText=I like a lot of things],

第二行TL1漏掉了最后一个更新Gavin,尽管按锁存器的意思在列表被修改后t1是可以访问的,
这说明了tl1中所包含的迭代器被tl2复制,并且最后一个更新对tl1是不可见的。这就是CopyOnWriteArrayList写时复制特性。

5 ConcurrentHashMap

ConcurrentHashMap(如果在多线程操作中,你希望某个线程修改了数据,其他线程可以同时获得最新的数据,可以尝试使用它)

ConcurrentHashMap类是标准ashMap的并发版本。它改进了Collections类中提供的synchronizedMap()功能,
因为那些方法返回的集合中包含的锁要比需要的多。

传统的HashMap用hash函数来确定存放键值对的”桶“,着意味着多线程处理可以更加简单直接,修改HashMap时并不需要把整个结构都锁住,只要锁住即将修改的桶就行了。

ConcurrentHashMap类实现了ConcurrentMap接口,提供了原子操作的新方法(只列出部分):

  • putIfAbsent():如果还没有对应键,则把键值对添加到HashMap中。

  • remove():如果对应键存在,且值也与当前状态相等(equal),则用原子方式移除键值对。

【ConcurrentHashMap的简要总结:
①public V get(Object key)不涉及到锁,也就是说获得对象时没有使用锁;
②put、remove方法会使用锁(也就是调用put、remove方法时其他线程会阻塞,直到某个线程操作完成才放开锁让其他线程操作数据,防止ConcurrentModificationException),但并不一定有锁争用,原因在于ConcurrentHashMap将缓存的变量分到多个Segment,每个Segment上有一个锁,只要多个线程访问的不是一个Segment就没有锁争用,就没有堵塞,各线程用各自的锁,ConcurrentHashMap缺省情况下生成16各Segment,也就是允许16个线程并发的更新而尽量没有锁争用;
③Iterator对象的使用,不一定是和其他线程更新同步,获得的对象可能是更新前的对象;ConcurrentHashMap允许一边更新、一边遍历,也就是说在Iterator对象遍历的时候,ConcurrentHashMap也可以进行remove、put操作,且遍历的数据会随着remove、out操作产生变化
(也即使说在多线程情况下,使用ConcurrentHashMap在某个线程进行remove和put操作后,在另一个线程能获取到最新修改的数据,而不会抛ConcurrentModificationException】

HashTable和ConcurrentHashMap的对比:

  • HashTable对get,put,remove都使用了同步操作,它的同步级别是正对HashTable来进行同步的,也就是说如果有线程正在遍历集合,其他的线程就暂时不能使用该集合了,这样无疑就很容易对性能和吞吐量造成影响,从而形成单点。
    而ConcurrentHashMap则不同,它只对put,remove操作使用了同步操作,get操作并不影响。
    当前ConcurrentHashMap这样的做法对一些线程要求很严格的程序来说,还是有所欠缺的,对应这样的程序来说,如果不考虑性能和吞吐量问题的话,个人觉得使用HashTable还是比较合适的;

  • HashTable在使用iterator遍历的时候,如果其他线程,包括本线程对HHashTable进行了put,remove等更新操作的话,就会抛出ConcurrentModificationException异常,但如果使用ConcurrentHashMap的话,就不用考虑这方面的问题了,ConcurrentHashMap能有效防止CurrentModificationException,它内部做了维护处理。
    在多线程下,在一个线程中使用ConcurrentHashMap对数据做处理,在另一个线程中获得的数据就是最新的处理后的数据。

ConcurrentHashMap<Integer, String> map = new ConcurrentHashMap<>();

for (int i = 0; i < 10; i++) {
    map.put(i, i + "");
}

Thread thread1 = new Thread(new Runnable() {
    @Override
    public void run() {
        map.put(11, "11"); // 在这个线程添加一个11
    }
});

Thread thread2 = new Thread(new Runnable() {
    @Override
    public void run() {
        for (Map.Entry<Integer, String> entry : map.entrySet() {
            System.out.println("key = " + entry.getKey() + ", value = " + entry.getValue()); // 能读取到最新加入的11
        }
    }
});

thread1.start();
thread2.start();

6 线程锁java.util.concurrent.locks(ReentrantLock)

Lock接口有它的两个实现类:

  • ReentrantLock:本质上跟在同步块上那种锁是一样的,但它要稍微灵活点儿。

  • ReentrantReadWriteLock:在需要读取很多线程而写入很少线程时,用它性能会更好。

举例:一个线程锁定后,通知另一个线程更新数据,如果另一个线程更新成功则释放锁让其他线程操作propagateUpdate()方法;如果另一个线程更新失败,则等待后继续让该线程操作直到成功释放锁(另一个线程操作成功,第一个线程的锁才会释放)该例能有效解决死锁问题

private final Lock lock = new ReentrantLock();
// 其他方法:
// boolean isLock = lock.tryLock();
// boolean isLock = lock.tryLock(time, TimeUnit.MILLISECONDS);

public void propagateUpdate(Update update, MicroBlogNode backup) {
     boolean acquired = false;
     boolean done = false;
     while (!done) {
         int wait = (int)(Math.random() * 10);
         try {
             acquired = lock.tryLock(wait, TimeUnit.MILLISECONDS);
             if (acquired) {
                 done = backupOther.tryConfirmUpdate(this, update); // 让其他线程更新
             }
         } finally {
             if (acquired) {
                 lock.unlock();
             }
         }

         // 如果false,释放锁并等待
         if (!done) {
             try {
                 Thread.sleep(wait);
             } catch(InterruptedException e) {
                 e.printStackTrance();
             }
         }
     }
 }

 public boolean tryConfirmUpdate(MicroBlogNode other, Update update) {
     boolean acquired = false;
     try {
         int wait = (int)(Math.random() * 10);
         acquired = lock.tryLock(wait, TimeUnit.MILLISECONDS);
         if (acquired) {
             return true;
         }
     } finally {
         if (acquired) {
             lock.unlock();
         }
     }
     return false;
 }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值