目录
线程
线程是进程的执行单元,一个进程可以有多个线程,一个线程必须有一个父进程。
线程和父进程的所有线程共享该进程的资源。
线程的执行是抢占式的。
多线程的优点:
- 进程之间不能共享内存,但线程可以
- 创建线程的代价小很多
- Java内置多线程的功能
使用线程
继承Thread
继承Tread类并重写run()方法。
package com.klaus.thread;
public class FirstThread extends Thread{
private int i;
public void run(){
for(;i < 5; i++){
System.out.println(getName() + " "+ i);
}
}
public static void main(String[] args) {
for (int i=0;i < 10; i++){
System.out.println(Thread.currentThread().getName());
if(i == 2){
new FirstThread().start();
new FirstThread().start();
}
}
}
}
实现Runnable
-
创建实现Runnable接口的类,并重写run()方法;
-
创建Runnable的实现类的实例,并以此为target创建Thread对象,该Thread对象才是真正的线程对象。
package com.klaus.thread;
public class SecondThread implements Runnable{
private int i;
@Override
public void run() {
for(;i < 5; i++){
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
public static void main(String[] args) {
for (int i=0; i < 10; i++){
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 5){
SecondThread st = new SecondThread();
new Thread(st, "新线程1").start();
new Thread(st, "新线程2").start();
}
}
}
}
使用Runnable接口创建的多个线程可以共享线程类的实例属性(上面的 i )。因为这种情况下,Runnable实现类只是Thread的target,而多个线程可以共享一个target。Thread类的作用就是把run()方法包装成线程执行体。
使用Callable和Future创建线程(Java 5)
Callable比Runnable更强大,提供了一个call()方法也可以作为线程执行体,并且call()可以有返回值,可以排除异常。
- 创建Callable·接口的实现类并实现call()方法,该call()将作为线程执行体,并由返回值。
- 创建Callable实现类的实例,使用FutureTask类包装Callable对象,该FutureTask对象封装了Callable对象的call()的返回值。
- 使用FutureTask对象作为Thread对象的target创建并启动新线程。
- 调用FutureTask对象的get()方法来获得子线程结束之后的返回值。
package com.klaus.thread;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
public class ThirdThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int i = 0;
for (; i < 100; i++)
System.out.println(Thread.currentThread().getName() + " " + i);
return i;
}
public static void main(String[] args) {
ThirdThread rt = new ThirdThread();
FutureTask<Integer> task = new FutureTask<>(rt);
for (int i = 0; i < 100; i++){
System.out.println(Thread.currentThread().getName());
if (i == 20)
new Thread(task).start();
}
try {
System.out.println("子线程返回:" + task.get());
}catch (Exception e){
e.printStackTrace();
}
}
}
比较
相较于继承Thread类,使用后面两种接口明显还可以继承其他类;共享一个target资源;但编程较复杂。
Runnable实例对象可以直接做Thread的target,而Callable对象需要被FutureTask包装后才可以。
线程的生命周期
–
线程被创建并启动后,并不是立即进入执行状态,也不是一直处在执行状态。线程生命周期中,有创建、就绪、运行、阻塞、死亡五个状态。
新建、就绪
线程被new时,是新建状态;调用start()后,线程有了方法调用栈、PC,处于就绪状态,至于何时运行取决于JVM。(只能对新建状态的线程调用start()方法)如果希望调用start()后子线程立马被执行,可以使用Thread.sleep(1)
让主线程睡眠一秒。
运行、阻塞
阻塞情况 | 解除阻塞 |
---|---|
线程调用sleep(),主动放弃所获得的运行资源 | 过了sleep()时间 |
调用阻塞式 I/O方法 | 调用的 I/O方法已经返回 |
请求某正在被其他线程持有的资源 | 成功得到请求资源 |
等待通知 | 收到通知 |
程序调用了suspend() | 被挂起的线程调用resume() |
线程进入阻塞状态后就只能进入就绪状态了,不能直接进入运行状态。
死亡
- 线程的run()或call()运行结束,线程死亡。
- 抛出未捕获异常,线程死亡。
- 调用线程的stop(),但容易形成死锁。
子线程启动后,和主线程同级,不会受主线程的影响。也就是主线程死亡后,子线程不会随着主线程一起死亡。
使用Thread的isAlive()方法可以判断线程是否死亡,处于新建状态、死亡状态时返回false。
控制线程
join线程
Thread提供了让一个线程等待另一个线程完成的方法----join()方法。某个程序调用其他线程的join方法时,主动调用的线程将被阻塞,直到被join的线程结束。
join方法还有两种重载方式:
join (); //等待被join的程序直到完成
join(long millis); //等待被join的线程最多millis毫秒
join(long millis, int nanos); //等待millis毫秒 + nanos毫微秒 !!精度太高,不用!!
如下,主线程创建了jt线程,然后调用了jt的join()方法,直到jt结束,主线程才继续执行。
package com.klaus.thread;
public class JoinThread extends Thread{
public JoinThread(String name){
super(name);
}
@Override
public void run() {
for (int i = 0; i < 4; i++) {
System.out.println(getName()+ " " + i);
}
}
public static void main(String[] args) throws Exception {
for (int i=0; i < 4; i++){
if (i ==1){
JoinThread jt = new JoinThread("新线程");
jt.start();
jt.join();
}
System.out.println(Thread.currentThread().getName() + " "+ i);
}
}
}
/* 输出
main 0
新线程 0
新线程 1
新线程 2
新线程 3
main 1
main 2
main 3
*/
后台线程
Thread提供了setDaemon()方法,将一个线程变为后台线程,不过在使用setDaemon()方法前,必须先调用该线程的staart()方法。当前台线程全部死亡时,后台进程也会随之死亡。
主线程默认是前台线程,前台线程创建的线程默认都是前台线程;后台线程创建的线程默认都是后台线程。
线程睡眠
Thread的静态方法sleep()也可以让正在执行的线程暂停一段时间并进入阻塞状态,在睡眠状态内,即使没有其他要执行的线程,处于sleep的线程也不会执行。
线程让步
yield()也是Thread的静态方法,它的作用是让线程暂停以下,不过不是进入阻塞状态而是就绪状态。调用用这个方法后,只有优先级比该线程高的或者相等的才有机会执行。
与sleep的区别
sleep是给所有线程让步,而yield只给优先级不低于自己的线程让步;
sleep将线程阻塞,yield是就绪;
sleep抛出了异常,yield没有;
sleep()比yield()有更好的移植性。
线程优先级
高优先级的线程有更多的机会执行,并不是优先执行。可以通过Thread的setPriority(int newPriority)来改变优先级,参数是1~10之间爱安定整数,main线程一般是普通优先级对应 5 。
线程同步
同步代码块
Java使用同步监视器解决不同步矛盾,使用的同步监视器的方法就是使用同步代码块:
synchronized (obj){
//同步代码块
}
任何时刻只有一个线程可以获得对同步监视器的锁定。线程在修改指定资源之前,先对该资源加锁,在加锁期间其他线程无法修改该资源,当线程修改完成后,释放对该资源的锁。
同步方法
使用synchronized
修饰某个方法,同步方法的同步监视器是this,也就是对象本身。线程安全是以运行效率作为代价的。synchronized可以修饰方法、代码块,但不能修饰构造器、属性等。
**注:**在单线程情况下,应该使用StringBuilder保证效率(因为这是线程不安全的);多线程情况下,使用StringBuffer。
同步锁的释放:
- 调用了线程的suspend(),虽然挂起,但不会释放同步监视器;
- 线程执行同步方法时,程序调用sleep(),yield()方法时,线程不会释放同步监视器。
同步锁
Java 5开始有了Lock,比synchronized更灵活。
class X{
//定义锁对象
private final ReentrantLock() lock = new ReentrantLock();
public void m(){
lock.lock();
try{
//需要保证线程安全的代码
}
finally{
lock.unlock();
}
}
}
当获取多个锁时,他们必须以相反的顺序释放“FILO"。
死锁
两个线程互相等待对方释放同步监视器时,就会发生死锁。
线程通信
传统线程通信
使用wait(),notify(),notifyAll()方法进行通信。
- 使用synchronizes修饰的方法,该类的默认实例就是同步监视器,可以直接调用这三个方法。
- 对于使用synchronized修饰的代码块,同步监视器是后面括号里的对象,所以必须使用该对象调用这三个方法。
这三个方法的解释如下:
- wait(),导致当前线程等待,直到其他方法该同步监视器的notify或notifiAll方法来唤醒该线程。调用wait()时会释放对同步监视器的锁。
- notify(),唤醒次同步监视器上等待的单个线程,如果有多个,随机唤醒一个。
- notifyAll(),唤醒同步监视器上所有等待线程。
使用Condition控制线程通信
前面是程序使用synchronized关键字保证同步,但如果使用的是Lock对象就没有隐式的同步监视器了,也就不能使用上面三种方法。于是就要Condition类的协助。Condition实例绑定在Lock对象上,要获得特定Lock实例的Condition实例就要通过lock.newCondition()
来创建。
Condition的方法:
- await(),类似于wait()。
- signal(),类似于notify()。
- signalAll(),类似于notifyAll()。
使用阻塞队列控制线程通信
阻塞队列(BlockingQueue)是Queue的子接口,但是作为线程同步的工具而不是一般容器。它有一个特征:当生产者线程试图向队列里添加元素时,如果队列已满,则线程被阻塞;当消费者尝试从队列里取出元素时,如果队列为空,则线程被阻塞。主要通过以下两个方法实现对应两个功能:
- put(E e),添加,若满,阻塞
- take(),取出,若空,阻塞
线程组
Java使用ThreadGroup控制线程组。默认情况下子线程和创建它的线程同属一个线程组一旦线程加入了某个线程组后,该线程将永远属于该线程组,直至线程死亡。Thread类提供了以下构造器来设置新创建的线程属于哪个线程组:
Thread(ThreadGroup threadGroup, Runnable target)
Thread(ThreadGroup threadGroup, Runnable target, String threadName)
Thread(ThreadGroup threadGroup, String threadName)
ThreadGroup提供了以下简单构造器:
ThreadGroup(String name);
ThreadGroup(ThreadGroup parent, String name); //指定父线程组
ThreadGroup有以下常用方法:
int activeGroup();
boolean interrupt();
boolean isDaemon();
void setDaemon(boolean daemon); //参数为true,将该线程变为守护线程
void setMaxPriority(int pri); //设置线程组的最高优先级
线程池
创建一个新线程的成本还是比较高的,所以用到线程池,线程池在启动时就创建了大量空闲线程,程序将一个Runnable对象或者Callable对象传给线程池,线程池就会启动一个线程执行里面的run或者call方法。方法执行完后,线程并不会死亡,而是再次返回线程池成为空闲状态,等待下一个对象。线程池中的最大线程数参数可以控制系统中并发线程数。
Java 5开始,java内建支持线程池。Java 5新增了Executors工程类来产生线程池:
newCacheThreadPool();
newFixedThreadPool(int n);//创建一个可重用的、具有n个线程的线程池
newSingleThreadExecutor();//创建一个单线程的线程池
newScheduledThreadPool(int corePoolSize);
//创建具有指定线程数的线程池,可以在指定延迟后执行线程任务.corePoolSize是指即使线程是空闲的也会被保存在线程池里的线程数
newSingleThreadScheduledExecutor();//创建只有一个线程的线程池,它可以在指定延迟后执行
包装线程不安全的类
Collection synchronizedCollection(Collection c); //返回线程安全的c
static List synchronizedList(List list);
static Map synchronizedMap(Map map);
static Set synchronizedSet(Set set);