2021-07-18
线程
线程是进程的执行单元,一个进程可以有多个线程,一个线程必须有一个父进程。
线程和父进程的所有线程共享该进程的资源。
线程的执行是抢占式的。
多线程的优点:
- 进程之间不能共享内存,但线程可以
- 创建线程的代价小很多
- 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"。
死锁
两个线程互相等待对方释放同步监视器时,就会发生死锁。
本文来自博客园,作者:klaus08,转载请注明原文链接:https://www.cnblogs.com/klaus08/p/15104988.html

浙公网安备 33010602011771号