并发(二)
本篇讲述一下多线程中,共享受限资源。
本文只是自己学习过程中的总结,知识点整理,会引用书中或是他人大量的代码或是观点。我会尽量标记出处。
参考文章:
http://blog.csdn.net/luoweifu/article/details/46613015
《Java编程思想》 参考了许多代码和观点,在此声明。
1.不正确的访问资源
请看例子:
1 public class ThreadDemo02 implements Runnable{ 2 3 private static int i = 0; 4 5 public void run() { 6 //synchronized (this) { 7 System.out.println(Thread.currentThread().getName() + " i value is " + i++); 8 //} 9 } 10 }
1 import java.util.concurrent.ExecutorService; 2 import java.util.concurrent.Executors; 3 import java.util.concurrent.TimeUnit; 4 5 public class ThreadDemo03 { 6 7 public static void main(String[] args) throws InterruptedException { 8 9 ThreadDemo02 demo02 = new ThreadDemo02(); 10 ExecutorService service = Executors.newCachedThreadPool(); 11 for (int i = 0; i < 10; i++) { 12 service.execute(demo02); 13 } 14 service.shutdown(); 15 } 16 }
这是实际执行的结果:
1 pool-1-thread-2 i value is 0 2 pool-1-thread-6 i value is 3 3 pool-1-thread-3 i value is 1 4 pool-1-thread-4 i value is 2 5 pool-1-thread-1 i value is 0 6 pool-1-thread-5 i value is 5 7 pool-1-thread-7 i value is 4 8 pool-1-thread-8 i value is 6 9 pool-1-thread-3 i value is 7 10 pool-1-thread-8 i value is 8
而我期待的结果是下面这样的:
1 pool-1-thread-1 i value is 0 2 pool-1-thread-6 i value is 1 3 pool-1-thread-5 i value is 2 4 pool-1-thread-7 i value is 3 5 pool-1-thread-4 i value is 4 6 pool-1-thread-3 i value is 5 7 pool-1-thread-2 i value is 6 8 pool-1-thread-8 i value is 7 9 pool-1-thread-5 i value is 8 10 pool-1-thread-8 i value is 9
这是因为共享资源竞争造成的结果。
2.解决共享资源竞争
2.1第一种方式是使用synchronized的方式。
在这里需要注意的是,synchronized锁的对象指的是同一对象内的。如果有两个对象,那么就会有两把锁,两把锁之间相互独立,没有影响。
在使用并发时,将域设置为private是非常重要的,否则,synchronized关键字就不能防止其他任务直接访问域,这样就会产生冲突。
syschronized是Java中的关键字,它修饰的对象有以下几种:
(1)修饰域,被称为同步代码块,作用的对象是调用这个方法的对象;
(2)修饰方法,作用的范围是整个方法,作用的对象是调用这个方法的对象;
(3)修饰静态方法,作用的对象是这个类的所有对象;
(4)修饰类,作用的对象是整个类的所有的对象。
在用synchronized修饰方法时要注意以下几点:
(1)synchronized关键字不能被继承。虽然,可以使用synchronized来定义方法,但是synchronized并不属于方法定义的一部分,因此它不能被继承。如果在父类中的某个方法使用了synchronized关键字,而在子类中覆盖了这个方法,那么子类的这个方法默认情况下并不是同步的,而必须显示第在子类的这个方法中加上synchronized关键字。当然,还可以在子类中调用父类的方法,虽然子类中的方法不是同步的,但是子类调用了父类的同步方法,因此,子类的方法也就相当于是同步了。
(2)构造方法不能使用synchronized关键字,但是在方法内部可以使用synchronized代码块。
(3)下面使用synchronized的方式是等价的。
1 public synchronized void method { 2 // to do 3 } 4 5 public void method { 6 synchronized(this) { 7 8 } 9 }
2.2使用显示的Lock对象
Lock对象必须被显式第创建、锁定、和释放,跟内建的锁形式相比,代码缺乏优雅性,但是更加灵活,赋予了你更加细粒度的控制力。这对于实现专用结构是很有用的,例如用于遍历链表中的节点的节节传递的加锁机制,这种遍历代码必须在释放当前节点的锁之前捕获下一个节点的锁。(红色部分观点摘自《Java 编程思想》)
这里我将JDBC的transaction联系起来,觉得这样大家会更好理解。
自己去设置上锁和解锁的时间点,跟事务开始结束的设置,我认为是一样的。
lock的代码如下:
1 import java.util.concurrent.locks.Lock; 2 import java.util.concurrent.locks.ReentrantLock; 3 4 public class ThreadDemo02_lock implements Runnable{ 5 6 private static int i = 0; 7 private Lock lock = new ReentrantLock(); 8 public void run() { 9 try { 10 lock.lock(); 11 System.out.println(Thread.currentThread().getName() + " i value is " + i++); 12 } finally { 13 lock.unlock(); 14 } 15 } 16 }
2.3原子性和易变性
原子性可应用于除long和double之外的所有基本类型之上的简单操作。但是当你定义long或double变量时,如果使用volatile关键字时,就会获得原子性。
volatile关键字确保了应用中的可见性。如果将一个域声明为volatile,那么是要对这个域产生了写操作,那么所有的读操作就都可以看到这个修改。
如果多个任务在同时访问某个域,那么这个域就应该是volatile的,否则,这个域就应该只能由同步来访问。
记住第一选择是synchronized,这是最安全的,而尝试其他任何方式都是有风险的。
在java中, i++等操作不是原子性的。
下面看一个例子:
1 public class ThreadDemo04 implements Runnable{ 2 3 private int i=0; 4 5 // 为了保证获取I的数值时,能达到稳定状态,应该同步地使用getValue,也就是加上synchronized关键字 6 public int getValue() { 7 // 虽然return i是原子性操作,但是缺少同步使得其数值可以在处于不稳定的中间状态时被读取。 8 return i; 9 } 10 11 @Override 12 public void run() { 13 while (true) { 14 evenIncrement(); 15 } 16 } 17 18 private synchronized void evenIncrement() { 19 i++; 20 i++; 21 } 22 23 public static void main(String[] args) { 24 ExecutorService service = Executors.newCachedThreadPool(); 25 ThreadDemo04 demo04 = new ThreadDemo04(); 26 service.execute(demo04); 27 while (true) { 28 int val = demo04.getValue(); 29 if(val%2 != 0) { 30 System.out.println(val); 31 System.exit(0); 32 } 33 } 34 } 35 }
2.4临界区
也称为同步控制块。进入代码之前,必须得到对象的锁,如果其他线程已经得到这个锁,那么就得等到锁被释放以后,才能进入临界区。
1 public class Pair { 2 3 private int x, y; 4 5 public Pair(int x, int y) { 6 this.x = x; 7 this.y = y; 8 } 9 10 public Pair() { 11 this(0, 0); 12 } 13 14 public int getX() { 15 return x; 16 } 17 18 public int getY() { 19 return y; 20 } 21 22 public void incrementX() { 23 x++; 24 } 25 26 public void incrementY() { 27 y++; 28 } 29 30 public String toString() { 31 return "x: " + x + ", y: " + y; 32 } 33 34 public class PairValuesNotEquationException extends RuntimeException { 35 36 private static final long serialVersionUID = 1L; 37 38 public PairValuesNotEquationException(){ 39 super("Pair values not equal: " + Pair.this); 40 } 41 } 42 43 public void checkState() { 44 if (x != y) { 45 throw new PairValuesNotEquationException(); 46 } 47 } 48 }
1 import java.util.ArrayList; 2 import java.util.Collections; 3 import java.util.List; 4 import java.util.concurrent.TimeUnit; 5 import java.util.concurrent.atomic.AtomicInteger; 6 7 public abstract class PairManager { 8 9 AtomicInteger checkCounter = new AtomicInteger(0); 10 protected Pair p = new Pair(); 11 private List<Pair> storange = Collections.synchronizedList(new ArrayList<Pair>()); 12 public synchronized Pair getPair() { 13 return new Pair(p.getX(), p.getY()); 14 } 15 protected void store(Pair p){ 16 storange.add(p); 17 try { 18 TimeUnit.MICROSECONDS.sleep(50); 19 } catch (InterruptedException ignore){ 20 21 } 22 } 23 24 public abstract void increment(); 25 }
1 public class PairManager1 extends PairManager { 2 3 @Override 4 public synchronized void increment() { 5 p.incrementX(); 6 p.incrementY(); 7 store(getPair()); 8 } 9 10 }
1 public class PairManager2 extends PairManager { 2 3 @Override 4 public void increment() { 5 6 Pair pair; 7 synchronized (this) { 8 p.incrementX(); 9 p.incrementY(); 10 pair = getPair(); 11 } 12 store(pair); 13 } 14 15 }
1 public class PairManipulator implements Runnable { 2 3 private PairManager pm; 4 public PairManipulator(PairManager pm) { 5 this.pm = pm; 6 } 7 @Override 8 public void run() { 9 10 while (true) { 11 pm.increment(); 12 } 13 } 14 15 public String toString () { 16 return "Pair: " + pm.getPair() + " checkCounter = " + pm.checkCounter.get(); 17 } 18 19 }
1 import java.util.concurrent.ExecutorService; 2 import java.util.concurrent.Executors; 3 import java.util.concurrent.TimeUnit; 4 5 public class CriticalSection { 6 7 static void testApproaches(PairManager pman1 , PairManager pman2) { 8 ExecutorService exec = Executors.newCachedThreadPool(); 9 PairManipulator pm1 = new PairManipulator(pman1); 10 PairManipulator pm2 = new PairManipulator(pman2); 11 PairChecker pch1 = new PairChecker(pman1); 12 PairChecker pch12= new PairChecker(pman2); 13 exec.execute(pm1); 14 exec.execute(pm2); 15 exec.execute(pch1); 16 exec.execute(pch12); 17 try { 18 TimeUnit.MICROSECONDS.sleep(500); 19 } catch (InterruptedException ex) { 20 System.out.println("sleep intertuped"); 21 } 22 System.out.println("pm1: " + pm1 + "\npm2: " + pm2); 23 System.exit(0); 24 } 25 26 public static void main(String[] args){ 27 PairManager pm1 = new PairManager1(); 28 PairManager pm2 = new PairManager2(); 29 testApproaches(pm1, pm2); 30 } 31 }
上面这个完成的例子,执行完后,就可以看到结果,从结果中可以看到同步控制块的效率要远高于同步方法。此外,也展示了如何把一个非保护类型的类,在其他类的保护和控制之下,应用于多线程的环境。
2.5在其他对象上同步
下面的示例展示了两个任务可以同时进入同一个对象,只要这个对象的方法是在不同的锁上同步的即可。
1 public class DualSynch { 2 3 private Object object = new Object(); 4 public synchronized void f() { 5 for (int i = 0; i < 5; i++) { 6 System.out.println("f()"); 7 Thread.yield(); 8 } 9 } 10 11 public void g() { 12 synchronized (object) { 13 for (int i = 0; i < 5; i++) { 14 System.out.println("g()"); 15 Thread.yield(); 16 } 17 } 18 } 19 }
1 public class SyncObject { 2 3 public static void main(String[] args) { 4 5 final DualSynch dualSynch = new DualSynch(); 6 7 new Thread(()-> dualSynch.f()).start(); 8 /* new Thread() { 9 public void run() { 10 dualSynch.f(); 11 } 12 }.start();*/ 13 dualSynch.g(); 14 } 15 }
2.6线程本地存储
线程本地存储是一种自动化机制,可以为使用相同变量的每个不同的线程都创建不同的存储。
例子如下:
1 public class Accessor implements Runnable { 2 3 private final int id; 4 public Accessor (int idn) { 5 id = idn; 6 } 7 @Override 8 public void run() { 9 while (!Thread.currentThread().isInterrupted()) { 10 ThreadLocalVariableHolder.increment(); 11 System.out.println(this); 12 Thread.yield(); 13 } 14 } 15 16 public String toString() { 17 return "#" + id + ": " + ThreadLocalVariableHolder.get(); 18 } 19 }
1 import java.util.Random; 2 import java.util.concurrent.ExecutorService; 3 import java.util.concurrent.Executors; 4 import java.util.concurrent.TimeUnit; 5 6 public class ThreadLocalVariableHolder { 7 8 private static ThreadLocal<Integer> value = new ThreadLocal<Integer>() { 9 private Random rand = new Random(47); 10 11 protected synchronized Integer initialValue() { 12 return rand.nextInt(100); 13 } 14 }; 15 16 public static void increment() { 17 value.set(value.get() + 1); 18 } 19 20 public static int get() { 21 return value.get(); 22 } 23 public static void main(String[] args) throws InterruptedException { 24 25 ExecutorService service = Executors.newCachedThreadPool(); 26 for (int i = 0; i < 4; i++) { 27 28 service.execute(new Accessor(i)); 29 } 30 TimeUnit.SECONDS.sleep(1); 31 service.shutdownNow(); 32 } 33 34 }
写到这里,这部分算是完事了,之而的文章将进入下一小节的学习了。