java并发编程基础(二)
本文简单介绍synchronized关键字,Lock的使用,原子性和可视性volatile,原子类,ThreadLocal.主要参考《thinking in java》等书籍的相关内容,记录本人在学习过程中的笔记.
synchronized关键字
-
synchronized方法:
synchronized void f()
-
防止多个线程同时访问资源,可以使用synchronized关键字,该关键字保护的代码片段会检查锁是否可用,然后获取锁,执行代码,释放锁.
-
共享资源一般是以对象形式存在的,也可以是文件,输入/输出端口,打印机…
-
理解细节:要控制共享资源的访问,可以先将它包装成为一个对象,然后所有访问这个对象的方法都需要标记为synchronized.如果某一个任务处于一个对标记为synchronized的方法调用中,那么在这个线程从该方法返回之前,其他所有调用类中的任何标记为synchronized方法的线程都会被阻塞.事实上,所有对象都会自动含有单一的锁(也称为监听器),在对象上调用任何的synchronized方法都会将该对象加锁.也就是说,对于某一个特定对象,所有的synchronized方法公用同一个锁.
-
值得注意的是,一个任务可以多次获得对象的锁.如果一个线程在某个对象中调用了一个synchronized方法,而该方法中又调用了该对象中的任意synchronized方法,这是不会发生死锁的.jvm会负责跟踪对象被加锁的次数,同一个任务多次调用某一个对象的synchronized方法,计数会累加.每离开一个synchronized方法,计数减一.当计数为0的时候,也就意味着对象锁被释放.
-
类似上面的第3点,针对每一个类,也有一个锁(作为类的Class对象的一部分),因此synchronized static可以在类的范围内防止对static数据的并发访问.
-
有时,我们只是希望防止多个线程访问方法内部的部分代码而不是防止访问整个方法,通过这种方式分离出来的代码称为临界区.如下代码,进入代码区域必须得到item锁
synchronized (item) { // accessed by one task at a time }
使用显式的Lock对象
-
使用:
class MyItem { private Lock lock = new ReentrantLock(); private int value = 0; public int next() { lock.lock(); try { value++; return value; } finally { lock.unlock(); } } }
-
上面程序需要注意
- lock.unlock()放在finally中,保证程序不论发生什么异常都可以释放锁.
- return语句要在try语句中出现,确保unlock不会过早发生.
- 在java中,类似
i++
或i += 2
这样的式子都不是原子性的 - 使用lock,如果系统抛出异常,都可以在finally中将系统维护在正确的状态.
-
ReetranLock更多的上锁api : https://segmentfault.com/q/1010000005602326
原子性和易变性
- 原子操作:不能被线程调度机制中断的操作.一旦操作开始,那么一定可以在可能发生的"上下文切换"之前执行完毕.但是,不要用原子性来代替同步
- 原子性可以用于除long和double之外的所有基本类型之上的"简单操作",这些操作可以保证它们会被当做不可分(原子)的操作执行.但是jvm将64位(long和double)的读取和写入当做两个分离的32位操作来执行,有可能在两个操作之间发生上下文切换,导致不同的任务可以看到不正确的结果.当定义long或者double变量时,使用volatile关键字,可以获得原子性(jdk 5之后)
- 在多处理器系统,一个任务做出了修改,即使在不中断的意义上讲是原子性,对其他任务也可能是不可视的(例如,修改只是暂时性存储在本地处理器的缓存中),因此不同任务有着不一样的视图.另一方面,同步机制会强制在处理器系统中,一个任务做出的修改在应用中必须是可视的.因此如果没有同步机制,那么修改时可视性将无法确定.
- volatile关键字:确保应用中的可视性,只要一个域被声明了volatile,然后对它进行写操作,那么所有的读操作都可以看到这个修改,即使使用了本地缓存,volatile也会立刻写入到内存中,而读取操作就发生在内存中.在非volatile域上的原子操作不必刷新到主存中,因此其它读取该域的任务也不必看到这个新值.如果多个任务在同时访问某一个域,那么这个域应该是volatile或者是通过同步访问的
- 一个任务所作的任何操作对这个任务都是可视的.
- 基本上,如果一个域可能被多个任务同时访问,或者这些任务中至少一个是写入任务,那么基本上就应该将这个域设置为volatile.将一个域设置为volatile,那么会告诉编译器不要执行任何移除读取和写入操作的优化,这些操作的目的是用线程中的局部变量维护对这个域的精确同步,实际上读取和写入都是针对内存的,而没有被缓存.
- 当然,如果多个任务对某一个值进行递增操作(可归纳为一个域的值依赖于它之前的值),即使使用volatile也避免不了问题,因为这是原子性的问题了.因此最安全的方式还是使用同步synchronized.
原子类
- AtomicInteger,AtomicLong,AtomicReference,即它们的操作方法具有原子性.
- 这些类被设计用来构建java.util.concurrent中的类,只有在特殊情况下才会在自己的代码中用到它们.一般使用synchronized和Lock.
线程本地存储ThreadLocal
-
防止任务在共享资源上产生冲突的第二种方式是根除变量的共享.线程本地存储是一种自动化机制,可以为使用相同变量的每一个不同的线程都创建不同的存储.
public class ThreadLocalDemo { private static ThreadLocal<Integer> map = new ThreadLocal<>(){ @Override protected Integer initialValue() { return 0; } }; public int getNextNum(){ map.set(map.get()+1); return map.get(); } private static class TestClient implements Runnable{ private ThreadLocalDemo demo; public TestClient(ThreadLocalDemo demo) { this.demo = demo; } @Override public void run() { for (int i = 0; i < 3; i++) System.out.println("Thread[" + Thread.currentThread().getName() + "] sn[" + demo.getNextNum() + "]"); } } public static void main(String[] args) { ThreadLocalDemo demo = new ThreadLocalDemo(); // 每一个线程共享,但是每一个线程都有自己的副本 ExecutorService service = Executors.newCachedThreadPool(); service.execute(new TestClient(demo)); service.execute(new TestClient(demo)); service.execute(new TestClient(demo)); service.shutdown(); } }
-
ThreadLocal对象通常当作静态存储.get方法返回与其线程相关联的对象的副本,set()会将参数插入到为其线程存储的对象中,并返回原有对象.