1、多线程所带来的风险
1)安全性:安全性的定义是“永远不发生糟糕的事情”,线程冲突问题
2)活跃性:活跃性的定义是“某件正确的事情最终会发生”,线程死锁问题
3)性能问题:性能问题的定义是“某件正确的事情尽快发生”,线程上下文切换,线程调度,线程同步等导致的额外的线程开销问题
2、线程安全性:当多个线程访问某一个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或者协同,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的
3、java.util.concurrent.atomic包:包含了一些原子变量类,用于实现在数值和对象引用上的原子状态转换(比如解决读-改-写问题)
package com.shma.jcip.chapter02; import java.io.IOException; import java.math.BigInteger; import java.util.concurrent.atomic.AtomicLong; import javax.servlet.GenericServlet; import javax.servlet.Servlet; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import com.shma.jcip.annotation.ThreadSafe; @ThreadSafe public class CountingFactorizer extends GenericServlet implements Servlet { private static final long serialVersionUID = -1716494625601923917L; /** * 原子操作类AtomicLong */ private static final AtomicLong count = new AtomicLong(0); public static long getCount() { return count.get(); } @Override public void service(ServletRequest request, ServletResponse resp) throws ServletException, IOException { BigInteger bigInteger = extractFromRequest(request); BigInteger[] factors = factor(bigInteger); count.incrementAndGet(); //相当于++i encodeIntoResponse(resp, factors); } public void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) { } BigInteger extractFromRequest(ServletRequest req) { return new BigInteger("7"); } BigInteger[] factor(BigInteger i) { // Doesn't really factor return new BigInteger[] { i }; } }
4、加锁机制:要保证状态的一致性,需要在单个原子操作中更新所有相关的状态变量
package com.shma.jcip.chapter02; import java.io.IOException; import java.math.BigInteger; import java.util.concurrent.atomic.AtomicReference; import javax.servlet.GenericServlet; import javax.servlet.Servlet; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import com.shma.jcip.annotation.NotThreadSafe; /** * 虽然lastNumber和lastFactors都是原子性的,其本身都是安全的,但是该类中存在竞态条件: * 在lastFactors缓存因子之积应该等于lastNumber中缓存的数值, * 当在不变性条件涉及到多个变量,各个变量之间并不是彼此独立的,而是某一个变量的值会对其他变量的值产生约束。 * 因此,当更新某一个变量时,需要在用一个原子操作中对其他变量同时进行操作 * @author admin * */ @NotThreadSafe public class UnsafeCachingFactorizer extends GenericServlet implements Servlet { private static final long serialVersionUID = 3132009232516672089L; private static final AtomicReference<BigInteger> lastNumber = new AtomicReference<BigInteger>(); private static final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<BigInteger[]>(); @Override public void service(ServletRequest req, ServletResponse resp) throws ServletException, IOException { BigInteger bigInteger = extractFromRequest(req); if(bigInteger.equals(lastNumber.get())) { encodeIntoResponse(resp, lastFactors.get()); } else { BigInteger[] factors = factor(bigInteger); lastNumber.set(bigInteger); lastFactors.set(factors); encodeIntoResponse(resp, factors); } } public void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) { } public BigInteger extractFromRequest(ServletRequest req) { return new BigInteger("7"); } public BigInteger[] factor(BigInteger i) { // Doesn't really factor return new BigInteger[]{i}; } } //线程安全 package com.shma.jcip.chapter02; import java.io.IOException; import java.math.BigInteger; import javax.servlet.GenericServlet; import javax.servlet.Servlet; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import com.shma.jcip.annotation.ThreadSafe; @ThreadSafe public class CachedFactorizer extends GenericServlet implements Servlet { private static final long serialVersionUID = 7619531648476154591L; private BigInteger lastNumber; private BigInteger[] lastFactors; private long hits; private long cacheHits; public synchronized long getHits() { return hits; } public synchronized double getCacheHitRatio() { return cacheHits * 1.0d / hits; } @Override public void service(ServletRequest req, ServletResponse resp) throws ServletException, IOException { BigInteger bigInteger = extractFromRequest(req); BigInteger[] factors = null; synchronized (this) { ++hits; if(bigInteger.equals(lastNumber)) { ++cacheHits; factors = lastFactors.clone(); } } if(factors == null) { factors = factor(bigInteger); synchronized (this) { lastNumber = bigInteger; lastFactors = factors.clone(); } } encodeIntoResponse(resp, factors); } void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) { } BigInteger extractFromRequest(ServletRequest req) { return new BigInteger("7"); } BigInteger[] factor(BigInteger i) { // Doesn't really factor return new BigInteger[]{i}; } }
5、重入:获取锁的粒度是线程,而不是请求调用某个线程,某一个线程试图获取它自己所持有的锁时,则可以成功。
重入一种实现方式是:为每一个锁分配一个计数器和一个所有者线程,初始化时计数器为0时,这个锁被认为没有线程持有,当线程请求一个未被持有的锁时,JVM会记下这个持有者线程,并计数器加1,如果同一个线程再次获取这个锁时,计数器递增,而当线程退出同步代码块时,计数器会相应地递减,直到计数器为0时,这个锁被释放
package com.shma.jcip.chapter02; public class Widget { public synchronized void doSomething() { } } /** * 子类先获取锁,在父类在获取同一个锁 * @author admin * */ class LoggingWidget extends Widget { @Override public synchronized void doSomething() { super.doSomething(); } }
6、加锁的含义不仅仅局限于互斥行为,还包含内存的可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作和写操作的线程都必须在同一个锁上同步
package com.shma.jcip.chapter03; import com.shma.jcip.annotation.NotThreadSafe; /** * 非线程安全的 * 三种输出情况: * 1) 正常输出42 * 2) 输出0,读线程只看到了ready值,没有看到number值 * 3) 死循环,读线程永远看不到ready值 * * 结论:在没有同步的情况下, 编译器、处理器以及运行时等都有可能对操作的执行顺序进行一些意想不到的调整。 * 在缺乏足够同步的多线程程序中,要想对内存操作的执行顺序进行判断,几乎无法得到正确的结论 * * 如果解决:只要有数据在多个线程中共享,就使用正确的同步 * @author admin * */ @NotThreadSafe public class NoVisibility { private static boolean ready; private static int number; private static class ReaderThread extends Thread { @Override public void run() { while(!ready) { Thread.yield(); } System.out.println(number); } } public static void main(String[] args) { ReaderThread thread = new ReaderThread(); thread.start(); number = 42; ready = true; } }
7、volatile:Java提供的稍弱的机制,用来确保变量的更新操作通知到其他线程。当变量声明为volatile类型后,编译器和运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会缓存到寄存器或者其他处理器不可见的地方。因此读取volatile类型的变量时永远返回最新写入的值。
理解volatile修饰的变量,可以将它们看作对其getter和setter方法设置了同步,执行读和写的时候都是调用其get和set方法,而这种访问不会执行加锁的操作,因此不会使执行线程阻塞。一般用作状态控制。
private volatile boolean state; //等价于 private boolean state; public synchroized void setState(boolean state) { this.state = state; } public synchroized boolean getState() { return state; }
8、构造函数逸出:如果对象构造完成之前就发布该对象,就会破坏线程的安全性。不要在构造过程中使用this引用逸出
package com.shma.jcip.chapter03; import com.shma.jcip.annotation.NotThreadSafe; @NotThreadSafe public class NotSafeListener { public NotSafeListener(EventSource source) { source.registerListener(new EventListener() { @Override public void onEvent(Event e) { doSomething(e); } }); } void doSomething(Event e) { } interface EventSource { void registerListener(EventListener e); } interface EventListener { void onEvent(Event e); } interface Event { } }
如果想在构造函数中注册一个事件的监听器或者启动线程,那么可以使用一个私有的构造函数和一个公共的工厂方法,从而避免不正确的构造过程
package com.shma.jcip.chapter03; import com.shma.jcip.annotation.ThreadSafe; @ThreadSafe public class SafeListener { private final EventListener listener; private SafeListener() { listener = new EventListener() { @Override public void onEvent(Event e) { doSomething(e); } }; } void doSomething(Event e) { } public static SafeListener newInstance(EventSource source) { SafeListener safeListener = new SafeListener(); source.registerListener(safeListener.listener); return safeListener; } interface EventSource { void registerListener(EventListener e); } interface EventListener { void onEvent(Event e); } interface Event { } } package com.shma.jcip.chapter03; import com.shma.jcip.annotation.ThreadSafe; @ThreadSafe public class SafeThread { private final Thread thread; private SafeThread() { thread = new Thread(new Runnable() { @Override public void run() { doSomething(); } }); } public void doSomething() { } public static SafeThread getInstance() { SafeThread safeThread = new SafeThread(); safeThread.thread.start(); return safeThread; } }
9、ThreadLocal:当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。从线程的角度看,目标变量就象是线程的本地变量。ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单:在ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本。当线程结束时,为这个线程分配的局部变量也会失效并回收。
1)T initialValue():线程第一次调用初始化方法,只执行一次
2)T get():返回当前线程所保存的局部变量
3)void set(T paramT):设置当前线程局部变量值
4)public void remove():当前线程局部变量的值删除,目的是为了减少内存的占用,加快内存回收的速度
package com.shma.threadlocal; public class TestNum { private ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>() { @Override protected Integer initialValue() { return 0; } }; public static void main(String[] args) { TestNum sn = new TestNum(); new TestClient(sn).start(); new TestClient(sn).start(); new TestClient(sn).start(); } public int getNextNum() { seqNum.set(seqNum.get() + 1); return seqNum.get(); } } class TestClient extends Thread { private TestNum sn; public TestClient(TestNum sn) { this.sn = sn; } @Override public void run() { for(int i=0; i<10; ++i) { try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("thread[" + Thread.currentThread().getName() + "] --> sn["+ sn.getNextNum() + "]"); } } }
ThreadLocal和线程同步机制比较:线程同步机制是共享同一份数据,通过保证同一时间只有一个线程可以访问的方式,ThreadLocal为每一个线程创建一个独立的变量副本,从而隔离每个线程之间的访问冲突;线程同步是用时间换空间的方式,而TheadLocal则是以空间换时间的方式。
import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class ConnectionManager { private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() { @Override protected Connection initialValue() { Connection conn = null; try { conn = DriverManager.getConnection( "jdbc:mysql://localhost:3306/test", "username", "password"); } catch (SQLException e) { e.printStackTrace(); } return conn; } }; public static Connection getConnection() { return connectionHolder.get(); } public static void setConnection(Connection conn) { connectionHolder.set(conn); } }
我们可以创建不同的ThreadLocal实例来实现多个变量在不同线程间的访问隔离,为什么可以这么做?因为不同的ThreadLocal对象作为不同键,当然也可以在线程的ThreadLocalMap对象中设置不同的值了。通过ThreadLocal对象,在多线程中共享一个值和多个值的区别,就像你在一个HashMap对象中存储一个键值对和多个键值对一样,仅此而已
10、不可变对象(Immutable Object):对象一旦被创建就不会被改变的对象,如果需要改变则生成一个新的不可变对象。实际上JDK本身就自带了一些immutable类,比如String,Integer以及其他包装类。为什么说String是immutable的呢?比如:java.lang.String 的trim,uppercase,substring等方法,它们返回的都是新的String对象,而并不是直接修改原来的对象。
1)immutable对象的状态在创建之后就不能发生改变,任何对它的改变都应该产生一个新的对象。
2)Immutable类的所有的属性都应该是final的。
3)对象必须被正确的创建,比如:对象引用在对象创建过程中不能泄露(leak)。
4)对象应该是final的,以此来限制子类继承父类,以避免子类改变了父类的immutable特性。
5)如果类中包含mutable类对象,那么返回给客户端的时候,返回该对象的一个拷贝,而不是该对象本身(该条可以归为第一条中的一个特例)
package com.shma.jcip.chapter03; import java.util.Date; import java.util.HashSet; import java.util.Set; import com.shma.jcip.annotation.ThreadSafe; /** * 不可变对象 * @author admin * */ @ThreadSafe public final class ImmutableReminder { private final Set<String> stooges = new HashSet<String>(); private final Date remindingDate; private final String name; private final String mobile; public ImmutableReminder(Date remindingDate, String name, String mobile) { super(); if(remindingDate.getTime() < System.currentTimeMillis()) { throw new IllegalArgumentException("Can not set reminder for past time: " + remindingDate); } this.remindingDate = new Date(remindingDate.getTime()); this.name = name; this.mobile = mobile; stooges.add("Moe"); stooges.add("Larry"); stooges.add("Curly"); } public boolean isStooge(String name) { return stooges.contains(name); } public Date getRemindingDate() { return new Date(remindingDate.getTime()); } public String getName() { return name; } public String getMobile() { return mobile; } }
每当需要对一组相关数据以原子方式执行某个操作时,就可以考虑创建一个不可变的类来包含这些数据,当线程获取该对象的引用后,就不必担心另外一个线程会修改对象的状态。如果要更新这些变量,可以创建一个新的容器对象,但其他是圆通原来对象的线程仍然看到对象处于一致状态
package com.shma.jcip.chapter03; import java.math.BigInteger; import java.util.Arrays; import com.shma.jcip.annotation.Immutable; @Immutable public class OneValueCache { private final BigInteger lastNumber; private final BigInteger[] lastFactors; public OneValueCache(BigInteger lastNumber, BigInteger[] lastFactors) { super(); this.lastNumber = lastNumber; this.lastFactors = Arrays.copyOf(lastFactors, lastFactors.length); } public BigInteger[] getFactors(BigInteger i) { if(lastNumber == null || !lastNumber.equals(i)) { return null; } return Arrays.copyOf(lastFactors, lastFactors.length); } }
package com.shma.jcip.chapter03; import java.io.IOException; import java.math.BigInteger; import javax.servlet.GenericServlet; import javax.servlet.Servlet; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import com.shma.jcip.annotation.ThreadSafe; @ThreadSafe public class VolatileCachedFactorizer extends GenericServlet implements Servlet { private static final long serialVersionUID = 2466605161267670532L; private volatile OneValueCache cache = new OneValueCache(null, null); @Override public void service(ServletRequest req, ServletResponse resp) throws ServletException, IOException { BigInteger i = extractFromRequest(req); BigInteger[] factors = cache.getFactors(i); if(factors == null) { factors = factor(i); cache = new OneValueCache(i, factors); } } void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) { } BigInteger extractFromRequest(ServletRequest req) { return new BigInteger("7"); } BigInteger[] factor(BigInteger i) { // Doesn't really factor return new BigInteger[]{i}; } }