多线程
创建线程
继承Thread类创建线程
步骤:
- 继承Thread类,重写run()方法,run()方法体为线程执行体。
- 创建Thread类的实例,即创建了线程对象。
- 调用线程对象的start()方法启动线程。
public class FirstThread extends Thread {
private int i;
@Override
public void run() {
for (;i<100;i++) {
System.out.println(getName()+" "+i);
}
}
public static void main(String[] args){
for (int i=0;i<100;i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
if (i == 20) {
new FirstThread().start();
new FirstThread().start();
}
}
}
}
上面程序共有3个线程,显式创建两个,还有一个main线程,是默认线程。当Java程序开始运行后,程序至少会创建一个主线程,主线程的线程执行体不是有run()方法确定的,而是由main()方法确定的,main()方法的方法体代表主线程的线程执行体。
实现Runnable接口
步骤:
- 实现Runnable接口,重写run()方法。
- 创建Runnable实现类的实例,并以其作为Thread的构造器参数target来创建Thread对象,该对象才是真正的线程对象。
public class SecondThread implements Runnable {
private int i;
@Override
public void run() {
for (;i<100;i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
public static void main(String[] args) {
for (int i=0;i<100;i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
if (i == 20) {
SecondThread secondThrea=new SecondThread();
new Thread(secondThrea,"新线程1").start();
new Thread(secondThrea,"新线程2").start();
}
}
}
}
Callable接口实现线程
该接口提供了一个call()方法可以作为线程执行体,但call()方法比run()方法功能更强大。
- call()方法可以有返回值。
- call()方法可以声明抛出异常。
Callable不是Runnable的子接口,不能作为Thread的target。Java5提供了Future接口来代表Callable接口里call()方法的返回值,并为Future接口提供了一个FutureTask实现类,该接口也实现了Runnable接口,它可以作为Thread类的target。
下面是使用Callable接口实现线程的方式。
List<Integer> list=new ArrayList<>(Arrays.asList(1,2,3,4,5));
FutureTask<Integer> futureTask=new FutureTask<>(()->{
int sum=0;
for (Integer i:list) {
sum+=i;
System.out.println(Thread.currentThread().getName()+"遍历的集合中的值:"+i);
}
return sum;
});
new Thread(futureTask).start();
try {
System.out.println("线程返回值sum="+futureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
运行结果如下:
Thread-0遍历的集合中的值:1
Thread-0遍历的集合中的值:2
Thread-0遍历的集合中的值:3
Thread-0遍历的集合中的值:4
Thread-0遍历的集合中的值:5
线程返回值sum=15
实现Runnable、Callable接口创建线程的优缺点:
1、还可以继承其它类
2、多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况。
劣势是:
编程稍微复杂,要访问当前线程,必须使用Thread.currentThread()方法。
线程的生命周期
当线程被创建并启动后,它既不是一启动就进入执行状态,也不是一直处于执行状态,在线程的生命周期中,他要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态。尤其是当线程启动以后,他不可能一直霸占着CPU独自运行,CPU需要在多条线程之间切换,于是线程状态也会多次在运行、就绪之间切换。
新建和就绪状态
当用new关键字创建一个线程之后,该线程就处于新建状态,此时他和其他Java对象一样,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值。此时的线程没有表现出任何线程的动态特征,程序不会执行线程的线程执行体。
当线程对象调用start()方法之后,该线程就处于就绪状态。Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态的线程并没有开始运行,只是表示该线程可以运行了。至于线程何时开始运行,取决于JVM里的线程调度器。
运行和阻塞状态
如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程就处于运行状态。
当发生如下情况时,线程将会进入阻塞状态:
- 线程调用sleep()方法主动放弃所占用的处理器资源。
- 线程调用了一个阻塞IO方法,在该方法返回之前,该线程被阻塞。
- 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。
- 线程在等待某个通知(notify)。
- 程序调用了线程的suspend()方法将该线程挂起。
针对以上几种情况,当发生如下特定的情况是可以解除上面的阻塞,让线程重新进入就绪状态。
- 调用sleep()方法的线程经过了特定的时间。
- 线程调用的阻塞式IO方法已经返回。
- 线程成功地获取了试图取得的同步监视器。
- 线程正在等待某个通知时,其他线程发出一个通知。
- 处于挂起状态的线程被调用了resume()方法。
线程死亡
- run()或call()方法执行完成,线程正常结束。
- 线程抛出一个未捕获的Exception或Error。
- 直接调用该线程的stop()方法来结束线程。
控制线程
join线程
Thread提供了让一个线程等待另一个线程完成的方法-join()。当在某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到被join()方法加入的线程执行完为止。
public class JoinThread extends Thread {
public JoinThread(String name) {
super(name);
}
public void run() {
for (int i=0;i<5;i++){
System.out.println(getName()+" "+i);
}
}
public static void main(String[] args) throws Exception{
for (int i=0;i<50;i++){
if(i==20){
JoinThread jt=new JoinThread("被joi的线程");
jt.start();
jt.join();
}
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
执行结果:
...
main 19
被joi的线程 0
被joi的线程 1
被joi的线程 2
被joi的线程 3
被joi的线程 4
main 20
....
后台线程
有一种线程,它是在后台运行的,它的任务是为其他线程提供服务,这种线程成为后台线程(Daemon Thread)。JVM的垃圾回收线程就是典型的后台线程。
后台线程有个特称:所有的前台线程都死亡,后台线程就会自动死亡。
调用Thread对象的setDaemon(true)方法可将指定线程设置成后台线程。
线程睡眠
如果需要让正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread类的静态sleep(long mills)方法来实现。当当前线程调用sleep()方法进入阻塞状态后,在其睡眠期间即使系统中没有其它可执行的线程,该线程也不会获得执行的机会。因此sleep()方法常用来暂停程序。
线程让步
yield()方法是一个和sleep()方法相似的方法,它也是Thread类提供的一个静态方法,它也可以让当前正在执行的程序暂停,但它不会阻塞该线程,它只是将该线程转入就绪状态。
改变线程的优先级
Thread类提供了setPriority(int priority)方法来改变线程的优先级,priority可以是一个整数,范围1~10,也可以使用如下三个静态常量:
/**
* The minimum priority that a thread can have.
*/
public final static int MIN_PRIORITY = 1;
/**
* The default priority that is assigned to a thread.
*/
public final static int NORM_PRIORITY = 5;
/**
* The maximum priority that a thread can have.
*/
public final static int MAX_PRIORITY = 10;
线程同步
synchronized实现不同线程之间的同步
Java使用synchronized关键字来实现代码在不同线程之间同步的。
synchronized的用法另起一篇做说明。
同步锁ReentrantLock
lock(),unlock()
使用Lock对象也可以实现同步效果,Lock是一个接口,它有ReentrantLock实现类。
Lock lock=new ReentrantLock();
与synchronized(obj),当前线程调用lock的lock()方法,表示当前线程占用lock对象,一旦占用,其它线程就不能占用了。与synchronized不同的是,一旦synchronized同步的块或方法结束,就会自动释放对obj对象的占用。lock必须调用unlock()方法进行手动释放锁。为了保证锁的释放,往往把unlock()放在finally里执行。
public class LockTest {
public static void main(String[] args) {
Lock lock=new ReentrantLock();
Thread thread1 = new Thread(){
@Override
public void run(){
System.out.println("线程1启动,试图占有对象lock");
lock.lock();
System.out.println("线程1已占有对象");
System.out.println("线程1进行5秒钟的业务操作");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("线程1结束,释放对象lock");
lock.unlock();
}
}
};
thread1.start();
try {
//先让线程1运行2秒
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread thread2 = new Thread(){
@Override
public void run() {
System.out.println("线程2启动,试图占有对象lock");
lock.lock();
System.out.println("线程2已经占有对象lock");
System.out.println("线程2进行5秒的业务操作");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("线程2结束,释放对象lock");
lock.unlock();
}
}
};
thread2.start();
}
}
运行结果如下:
线程1启动,试图占有对象lock
线程1已占有对象
线程1进行5秒钟的业务操作
线程2启动,试图占有对象lock
线程1结束,释放对象lock
线程2已经占有对象lock
线程2进行5秒的业务操作
线程2结束,释放对象lock
可以看到线程1占有对象lock,进行业务处理期间,线程2是获取不到锁的,直到线程1结束,释放锁。
tryLock()
synchronized是不占用到锁,就一直试图占用下去,与synchronized不一样,Lock接口提供了一个trylock()方法。trylock()方法会在指定的时间范围内试图占用,占用成功了,就OK。如果在指定时间里没有占用到,就不会再去等待。
public class LockTest {
public static void main(String[] args) {
Lock lock=new ReentrantLock();
Thread thread1 = new Thread(){
@Override
public void run(){
System.out.println("线程1启动,试图占有对象lock");
lock.lock();
System.out.println("线程1已占有对象");
System.out.println("线程1进行5秒钟的业务操作");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("线程1结束,释放对象lock");
lock.unlock();
}
}
};
thread1.start();
try {
//先让线程1运行2秒
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread thread2 = new Thread(){
@Override
public void run() {
System.out.println("线程2启动,试图占有对象lock");
// lock.lock();
boolean locked= false;
try {
locked = lock.tryLock(1, TimeUnit.SECONDS);
if (locked) {
System.out.println("线程2已经占有对象lock");
System.out.println("线程2进行5秒的业务操作");
Thread.sleep(5000);
}else{
System.out.println("线程2经过1秒钟的的努力,没有占用到对象,放弃占有");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (locked) {
System.out.println("线程2结束,释放对象");
}else{
System.out.println("线程2没占用到对象,直接结束");
}
}
}
};
thread2.start();
}
}
打印结果如下:
线程1启动,试图占有对象lock
线程1已占有对象
线程1进行5秒钟的业务操作
线程2启动,试图占有对象lock
线程2经过1秒钟的的努力,没有占用到对象,放弃占有
线程2没占用到对象,直接结束
线程1结束,释放对象lock
可以看到线程试图占用对象,但经过1秒钟还没有占用到,就放弃了。
线程通信
多线程之间通过被加锁对象的wait()
和notify()
方法进行通信。
比如有A,B两个线程处于运行状态,A线程先获得对象obj的锁,如果在A线程体内,obj调用了wait(),那么A线程就会暂时放弃对obj对象的锁的占有,B线程就会获得对obj对象的锁;B线程执行期间如果对象obj调用了notify()方法,那么处于等待状态的线程,也即B线程,就会重新获得对obj对象的锁。
还有一个notifyAll()
方法,顾名思义,就是唤醒所有等待状态的线程。
注意:wait,notify,nitifyAll并不是Thread线程上的方法,它们是Object上的方法。
因为所有的Object都可以被用来作为同步对象,所以准确的讲,wait和notify是同步对象上的方法。
wait()的意思是: 让占用了这个同步对象的线程,临时释放当前的占用,并且等待。 所以调用wait是有前提条件的,一定是在synchronized块里,否则就会出错。
notify() 的意思是,通知一个等待在这个同步对象上的线程,你可以苏醒过来了,有机会重新占用当前对象了。
notifyAll() 的意思是,通知所有的等待在这个同步对象上的线程,你们可以苏醒过来了,有机会重新占用当前对象了。
死锁
当业务比较复杂,多线程应用里有可能会发生死锁。
- 线程1 首先占有对象obj1,接着试图占有对象obj2
- 线程2 首先占有对象obj2,接着试图占有对象obj1
- 线程1 等待线程2释放对象obj2
- 与此同时,线程2等待线程1释放对象obj1
public class DeadLockTest {
public static void main(String[] args) {
Object obj1=new Object();
Object obj2=new Object();
Thread thread1 = new Thread() {
@Override
public void run() {
synchronized (obj1) {
System.out.println("thread1 已占有obj1");
try {
//模仿线程1在处理业务
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread1 试图占有obj2,等待中.......");
synchronized (obj2) {
System.out.println("不能输出此句,则发生死锁");
}
}
}
};
thread1.start();
Thread thread2 = new Thread(){
@Override
public void run() {
synchronized (obj2){
System.out.println("thread2 已占有obj2");
try {
//模仿线程2在处理业务
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("thread2 试图占有obj1,等待中........");
synchronized (obj1){
System.out.println("不能输出此句,则发生死锁");
}
}
}
};
thread2.start();
}
}
运行结果:
thread1 已占有obj1
thread2 已占有obj2
thread2 试图占有obj1,等待中........
thread1 试图占有obj2,等待中.......
线程池
系统启动一个新的线程的成本是比较高的,因为它涉及与操作系统交互。使用线程池可以很好地提高性能,尤其是当程序中需要创建大量生存期很短暂的线程时,就更应该考虑使用线程池。除此之外,当系统中包含大量并发线程时,会导致系统性能剧烈下降,甚至导致JVM崩溃,而线程池的最大线程数参数可以控制系统中并发线程数不超过此数。
在Java5以前,开发者必须手动实现自己的线程池;从Java5开始,Java内建支持线程池。Java5新增了一个Executors工厂类来产生线程池。
public class TestThread {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(6);
Runnable target=()->{
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+"的i值为:"+i);
}
};
pool.submit(target);
pool.submit(target);
pool.shutdown();
}
}
ForkJoinPool
public class PrintTask extends RecursiveAction {
private static final int THREADHOLD =50;
private static final long serialVersionUID = 5204454735323133216L;
private int start;
private int end;
public PrintTask(int start,int end){
this.start=start;
this.end=end;
}
@Override
protected void compute() {
if (end - start < THREADHOLD) {
for(int i=start;i<end;i++) {
System.out.println(Thread.currentThread().getName()+"的i值:"+i);
}
}else {
int middle=(start+end)/2;
PrintTask left=new PrintTask(start,middle);
PrintTask right=new PrintTask(middle,end);
left.fork();
right.fork();
}
}
public static void main(String[] args) {
ForkJoinPool pool=new ForkJoinPool();
pool.submit(new PrintTask(0,300));
try {
pool.awaitTermination(2, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
pool.shutdown();
}
}
原子操作
所谓原子操作是不可中断的操作,比如赋值操作:int i=3;
原子操作是线程安全的,但i++这个行为,是由3个原子操作组成:
步骤1: 取i的值;
步骤2: 计算i+1;
步骤3: 把新值赋给i;
这三个步骤,每一步都是原子操作,但合在一起,就不是原子操作了。所以可能发生这样的情况,一个线程在步骤1取得i的值后,还没来得及进行步骤2,另一个线程就取得i的值了。所以i++这个操作不是线程安全的。
解决方案:使用AtomicInteger类。
Java6新增了一个包java.util.concurrent.atomic,里面有各种原子类,比如AtomicInteger,它提供了各种自增、自减原子性的,也就是线程安全的。
下面例子演示了这两种情况:
public class AtomicTest {
private static int value=0;
private static AtomicInteger atomicInteger=new AtomicInteger();
public static void main(String[] args) {
int num=100000;
Thread[] threads=new Thread[num];
for (int i=0;i<num;i++) {
Thread thread = new Thread(){
@Override
public void run() {
value++;
}
};
thread.start();
threads[i]=thread;
}
//在main线程里join这些线程,等待这些线程全部执行结束,在执行main线程
for (Thread thread : threads) {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.printf("%d个线程进行value++后,value的值变为:%d\n,",num,value);
Thread[] threads1=new Thread[num];
for (int i=0;i<num;i++) {
Thread thread = new Thread(){
@Override
public void run() {
atomicInteger.incrementAndGet();
}
};
thread.start();
threads1[i]=thread;
}
//在main线程里join这些线程,等待这些线程全部执行结束,在执行main线程
for (Thread thread : threads1) {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.printf("%d个线程进行atomicInteger.intValue()操作后,atomicInteger的值变为:%d\n",num,atomicInteger.intValue());
}
}
上面程序输出结果为:
100000个线程进行value++后,value的值变为:99995
100000个线程进行atomicInteger.intValue()操作后,atomicInteger的值变为:100000
线程相关的类
ThreadLocal类
ThreadLocal代表一个线程局部变量,通过把数据放在ThreadLocal中就可以让每个线程创建一个该变量的副本,从线程的角度看,就好像每个线程都完全拥有该变量一样。
class Account{
private ThreadLocal<String> name=new ThreadLocal<>();
public Account(String str){
name.set(str);
System.out.println("-----"+name.get());
}
public String getName() {
return name.get();
}
public void setName(String str) {
name.set(str);
}
}
class MyTest extends Thread {
private Account account;
public MyTest(Account account, String name) {
super(name);
this.account=account;
}
@Override
public void run() {
for (int i=0;i<5;i++) {
if (i == 3) {
account.setName(getName());
}
System.out.println(account.getName()+"账户的i值:"+i);
}
}
}
public class ThreadLocalTest {
public static void main(String[] args) {
Account at=new Account(Thread.currentThread().getName()+"线程");
new MyTest(at,"线程A").start();
new MyTest(at,"线程B").start();
}
}
输出结果为:
-----main线程
null账户的i值:0
null账户的i值:1
null账户的i值:2
线程A账户的i值:3
线程A账户的i值:4
null账户的i值:0
null账户的i值:1
null账户的i值:2
线程B账户的i值:3
线程B账户的i值:4
上面有三个线程,隐式的main,两个MyTest线程,三个线程都拥有account这个实例变量,但account中的name是ThreadLocal类型的,所以每个线程都拥有该变量name的副本,各个线程对name的操作互不影响。需要注意的是在main主线程上为name赋值为“主线程”,但并不影响另两个线程name的值。
线程安全的集合
从Java5开始,在java.util.concurrent包下提供了大量支持高效并发访问的集合接口和实现类。
- 以Concurrent开头的集合类,如ConcurrentHashMap,ConcurrentSkipListMap。
- 以CopyOnWrite开头的集合类,如CopyOnWriteArrayList。
其中以Concurrent开头的集合类代表了支持并发访问的集合,它们可以支持多个线程并发写入访问,这些写入线程的所有操作都是安全的,但读取操作不必锁定。
CopyOnWriteArrayList执行写入操作时需要频繁地复制数组,性能比较差,但由于读操作与写操作不是操作同一个数组,而且读操作也不需要加锁,因此读操作就很快、很安全。CopyOnWriteArrayList适合用在读取操作远远大于写入操作的场景中,如缓存等。