本文旨在用最通俗的语言讲述最枯燥的基本知识。
全文提纲:
1.线程是什么?(上)
2.线程和进程的区别和联系(上)
3.创建多线程的方法(上)
4.线程的生命周期(上)
5.线程的控制(上)
6.线程同步(下)
7.线程池(下)
8.线程通信(下)
9.线程安全(下)
10.ThreadLocal的基本用法(下)
上集已经讲述了Java线程的一些基本概念,本文接下来讲述的是Java的一些高级应用。
一开始接触“线程同步”这个概念可以有点难以理解,我们来举个栗子:
爸爸开了一张银行卡存进去10000块钱,是留给在山东读大学的哥哥和在河南老家读高中的妹妹用的。哥哥前天取了2000,变成8000,妹妹昨天取了1000,剩余7000,今天他们同时到银行同时取钱,哥哥打开时ATM发现有7000余额,妹妹打开时也发现是7000余额,他们同时按下确定取1000钱,当他们取完钱之后在查看余额发现只有5000块钱,都在想我只取了1000啊怎么扣了我2000呢?
这就是生活中的“同步”问题了。
我们把思维转入到这个ATM的后台程序,幸好后台程序对取钱的操作做了同步动作的监听器,能在多线程同时操作的过程中把取钱的动作给锁定起来,如果程序没有处理同步问题,那两边的ATM的算术都是:7000-1000,结果是剩余6000.这样子,银行对账就会出错了。
因此可见,并发编程不合理使用也会带来一些弊端,而针对多线程并发的问题,Java引入了同步监视器来解决问题:当线程要执行同步代码块/方法之前,必须先获得对同步监视器的锁定。
Java中锁用在的地方有:
1.代码块同步
语法:
1synchronized (obj) { 2//同步内容(比如取钱的操作) 3} 复制代码
其中obj就是同步监视器,也就是说任何线程要进入执行该代码块之前,首先获得对obj的锁定,获得之后,其它线程就无法获取它,修改它,直到当前线程释放位置。
比如:爸爸的银行卡账户
1public BankCardAccount bankAccount; 2synchronized (bankAccount) { 3//对bankAccount的扣钱动作 4} 复制代码
当哥哥和妹妹同时取钱时,就如同两个线程在执行,当其中一个线程获取到对bankAccount的锁定时,另一个线程必须等待当前线程用完之后释放bankAccount的锁定,才可以获得并且修改之
2.方法同步
语法:
1修饰符 synchronized 返回值 方法名(形参列表){ 2} 复制代码
方法的同步不需要显示指定同步监视器,因为它的同步监视器就是当前类的对象,也就是this。
3.锁释放
有锁定就需要有释放,同步监视器的锁释放的事件有以下情况:
而不释放的事件也有如下:
4. 同步锁
对于基本的同步问题,synchronized就可以满足,但是需要对线程的同步有更强大的操作,就需要到同步锁Lock了
Lock是控制多线程对共享资源进行访问的工具,通常,所提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前首先要获得Lock对象。
Lock针对不同的使用场景提供了多种类/接口,主要有以下:
1. Lock接口
Lock接口提供了几个方法来操作锁:
1package java.util.concurrent.locks; 2import java.util.concurrent.TimeUnit; 3//Lock接口 4public interface Lock { 5 //获取锁。如果锁已被其他线程获取,则进行等待 6 void lock(); 7 //获取锁,在等待过程中可以相应中断等待状态 8 void lockInterruptibly() throws InterruptedException; 9 //尝试获取锁,返回true为获得成功,返回false为获取失败 10 //它和lock()不一样的是,它不会一直等待,而是尝试获取,立即返回 11 boolean tryLock(); 12 //尝试获得锁,如果获取不到就等待time时间 13 boolean tryLock(long time, TimeUnit unit) throws InterruptedException; 14 //释放锁 15 void unlock(); 16} 复制代码
2. ReentrantLock
可重入锁。意思是同一个线程可以多次获取同一个锁,虽然synchronized也属于可重入锁,但是synchronized是在获取锁的过程中是不可中断的,而ReentrantLock则可以。
ReentrantLock是唯一实现了Lock接口的类,因此我们在可以这样创建一个Lock对象:
1Lock l=new ReentrantLock(); 复制代码
ReentrantLock的默认状态和synchronized获得的属于非公平锁( 抢占式获得锁,先等待(调用lock())的线程不一定先获得锁,而公平锁则是先获得lock的线程现货的锁 )。但是ReentrantLock可以设置为公平锁,如:
1//公平锁 2Lock l1=new ReentrantLock(true); 3//非公平锁 4Lock l2=new ReentrantLock(false); 复制代码
3. ReadWriteLock
顾名思义,它叫做读写锁,是一个接口,用来管理读锁和写锁,读锁也叫共享锁,也就是说读锁可以被多个线程共享,写锁也称排他锁,意思是,当一个线程获得了写锁,其它线程只能等待,不能共享。
前面我们说到:多线程并发带来同步问题,而同步问题用同步监听器来解决问题。
但我们发现有这样的一个怪圈:
多线程为了提高程序执行效率,同步监听器为了是多线程执行时有且只有其中一个线程能执行synchronized修饰的代码块或者方法,这两个东西有着此消彼长的关系.
那么?怎么样才能让多线程能愉快的行走,而同步问题有可以尽可能少的出现呢?
其实读写锁在一定程度上能解决这个难题。它的特性是:
也就是说,比如程序开多个线程对一个文件进行读写操作时,如果用synchronized,则读写操作要互相等待,而有了ReadWriteLock之后
我们可以把读写的锁操作分开,读文件操作用读锁,写文件操作用写锁,
这样就可以快运行效率了。
我们来看它的源码:
1public interface ReadWriteLock { 2 //获取读锁 3 Lock readLock(); 4 //获取写锁 5 Lock writeLock(); 6} 复制代码
只有一个获取读锁和一个获取写锁的接口方法,接口的存在得有有类实现它才有意义,我们看下一个类:
4. ReentrantReadWriteLock
ReentrantReadWriteLock是ReadWriteLock接口的实现类,当我们要创建一个ReadWriteLock的锁时,通常:
1ReadWriteLock rl=new ReentrantReadWriteLock(); 复制代码
前面说到ReentrantLock是Lock的实现类,ReentrantLock是一种排它锁,也就是说某个时间内,只有允许一个线程访问(但是这个线程可以同时访问多次),而ReentrantLock是读写锁,也就是说在同一时间内,允许多个线程同时获取读锁进行操作(但不允许读写、写写同时操作),在某些业务场景(比如读操作远高于写操作)下,ReentrantReadWriteLock会比ReentrantLock有更好的性能和并发。
ReentrantReadWriteLock主要有以下特效:
1//公平锁 2ReadWriteLock rl=new ReentrantReadWriteLock(true); 3//非公平锁 4ReadWriteLock rl=new ReentrantReadWriteLock(); 复制代码
当线程在程序中执行时,线程的调度有一些不确定性,也就是在常规情况无法准确的控制线程之间的轮换执行时机,因此Java提供了一些机制来便于开发者控制线程的协调运行。
1. synchronized修饰方法/代码块中使用wait()、notify()、notifyAll()协调
实际上,wait、notify、notifyAll是定义在Object类的实例方法他们只能在synchronized的代码块/方法中使用,用来控制线程。
2. 使用condition控制
对于用Lock来做同步工作的情况,Java提供了condition类来协助控制线程通信。condition的实例是由Lock对象来创建的,
1//创建一个lock对象 2Lock l=new ReentrantLock(); 3//创建一个condition实例 4Condition con=l.newCondition(); 复制代码
Condition类有以下方法:
3. 使用阻塞队列控制
在Java5中提供了一个接口: BlockingQueue ,它是作为线程同步的一个工具而产生,当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则线程被阻塞,当消费者线程试图从BlockingQueue中取出元素时,如果队列为空,则线程被阻塞。
BlockingQueue接口源码:
1public interface BlockingQueue<E> extends Queue<E> { 2 boolean add(E e); 3 boolean offer(E e); 4 void put(E e) throws InterruptedException; 5 boolean offer(E e, long timeout, TimeUnit unit) 6 throws InterruptedException; 7 E take() throws InterruptedException; 8 E poll(long timeout, TimeUnit unit) 9 throws InterruptedException; 10 int remainingCapacity(); 11 boolean remove(Object o); 12 public boolean contains(Object o); 13 int drainTo(Collection<? super E> c); 14 int drainTo(Collection<? super E> c, int maxElements); 15} 复制代码
其中支持阻塞的有两个:
BlockingQueue接口的实现类有:
阻塞队列平时用得少,就仅仅讲述一些基本原理和使用方法,例子不再赘述。
线程池的产生和数据库的连接池类似,系统启动一个线程的代价是比较高昂的,如果在程序启动的时候就初始化一定数量的线程,放入线程池中,在需要是使用时从池子中去,用完再放回池子里,这样能大大的提高程序性能,再者,线程池的一些初始化配置,也可以有效的控制系统并发的数量。
Java提供了一个Executors工厂类来创建线程池,要新建一个线程池,主要有以下几个静态方法:
关于每个方法具体使用以及参数,再次就不赘述了,有兴趣的筒子直接进入Executors类就可以看到了。
什么是线程安全?
在多线程环境下,多个线程同时访问共享数据时,某个线程访问的被其它线程修改了,导致它使用了错误的数据而产生了错误,这就引发了线程的不安全问题。
而当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
大家是否记得,不管是老师的课后习题还是面试笔试题,经常都会出现“StringBuilder、StringBuffer是否线程安全”这样的问题?
我们来查看各自的源码看看究竟吧。
StringBuffer的append方法:
1@Override 2 public synchronized StringBuffer append(String str) { 3 toStringCache = null; 4 super.append(str); 5 return this; 6 } 复制代码
StringBuilder的append方法:
1 @Override 2 public StringBuilder append(String str) { 3 super.append(str); 4 return this; 5 } 复制代码
再看看它们的super.append源码:
1public AbstractStringBuilder append(String str) { 2 if (str == null) 3 return appendNull(); 4 int len = str.length(); 5 ensureCapacityInternal(count + len); 6 str.getChars(0, len, value, count); 7 count += len; 8 return this; 9 } 复制代码
可以看出,两者的append方法区别就在于前者有synchronized修饰,这意味着多个线程可以同时访问这个方法时,前者是阻塞运行的,而后者是可以同时运行并且同时访问count,因此就有可能导致count错乱。由此可见:
StringBuffer 是线程安全的,但是由于加了锁,导致效率变低。
StringBuilder 是线程不安全的,在单线程环境下,效率非常高。
既然已经从根本知道了什么是线程安全,那么Java是如何解决线程安全问题的呢?
从Java5开始,增加一了些线程安全的类来处理线程安全的问题,如:
ThreadLocal代表一个线程局部变量,通过把数据放在ThreadLocal中就可以让每个线程创建一个该变量的副本,从未避免并发访问的线程安全问题。
维持线程封闭性的一种方法是使用ThreadLocal。它提供了set和get等访问方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get方法总是返回由当前执行线程在调用set时设置的最新值。
它提供三个方法:
举个栗子:创建一个带有ThreadLocal的类:
1public class TestThreadLocal { 2 // 副本 3 private ThreadLocal<Integer> countLoacl = new ThreadLocal<Integer>(); 4 public TestThreadLocal(Integer num) { 5 countLoacl.set(num); 6 } 7 public Integer getCount() { 8 return countLoacl.get(); 9 } 10 public void setCount(Integer num) { 11 countLoacl.set(num); 12 } 13} 复制代码
这样子创建的类带有ThreadLocal的countLoacl,在多个线程同时消费这个对象时,ThreadLocal会为每个线程创建一个countLoacl副本,这样就可以避免多线程之间的资源竞争而导致安全问题了。