和我一起迎接明天的太阳吧

klaus08

焦虑源于行动的匮乏

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

  1. 创建实现Runnable接口的类,并重写run()方法;

  2. 创建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()可以有返回值,可以排除异常

  1. 创建Callable·接口的实现类并实现call()方法,该call()将作为线程执行体,并由返回值。
  2. 创建Callable实现类的实例,使用FutureTask类包装Callable对象,该FutureTask对象封装了Callable对象的call()的返回值。
  3. 使用FutureTask对象作为Thread对象的target创建并启动新线程。
  4. 调用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"。

死锁

两个线程互相等待对方释放同步监视器时,就会发生死锁。

posted @ 2021-07-18 18:37  klaus08  阅读(32)  评论(0)    收藏  举报