Java多线程基础

在入坑Java多线程之前,先大概了解一下进程和线程的区别。

进程与线程的区别

CPU一次只能运行一个进程,该进程运行期间其他的进程都属于非运行状态。而一个进程又包括了多个线程,该进程的内存也由这些线程共享。

虽然是内存共享,但是在并发场景下,为了防止多个线程同时读写某一块内存,某个线程也可以通过互斥锁mutex让其占用内存,其他线程若要使用这块内存,则必须等待持有锁的线程执行结束后才能使用。就好比一间厕所,一次只能有一个人占坑,其他人只能在门外等待。

对于某些内存区域只能给固定数目的线程使用,那为了限制内存区域中执行的线程数目,可以通过信号量semaphore保证多个线程间不会相互冲突。信号量的实现方式和互斥锁类似,可以把互斥锁看做为信号量的特殊情况(n=1)。就好比一间民宿一共有3把钥匙,一次只能住3个人,若有人退房了就把钥匙交给下一个住户。

 

总的来说:

①线程不能独立存在,它必须依托于进程。

②进程有自己独立的地址空间,互相不影响。线程没有自己独立的地址空间,它只是进程的多个执行路径。也就是说进程挂了不会影响其他进程,线程挂了进程也跟着挂了。

③进程的切换比线程开销大,对于某些要求同时进行且需要共享某些变量的并发操作必须采用线程。

④Java采用单线程编程模型,若没有自己创建线程,程序则会自动创建主线程。比如我们平时执行main方法时(没有自己创建线程),就会由Java程序自己创建一个主线程执行main方法。


 

在了解了什么是线程后,接下来步入正题

start和run方法

run()只是当前线程的一个普通方法调用。

start()其本质是调用了Thread类中的native方法start0(),而start0()底层又调用了JVM_StartThread这个方法,开辟一个新的线程并用该线程执行run方法。


 

Thread和Runnable有什么区别

Thread是一个类,它实现了Runnable接口。

对于Runnable接口,我们可以在其源码中观察到,它只有一个抽象run方法。可以看出,Runnable接口本身是不具备多线程的特性的。

public interface Runnable {
    public abstract void run();
}

对于Thread类,我们可以在其源码中观察到,它有多个构造方法可以都接收一个Runnable类型的参数,下面的代码只是其中之一。这就意味着我们可以通过Thread传入一个Runnable类型的实例,实现它的多线程特性。

    public Thread(Runnable target, String name) {
        init(null, target, name, 0);
    }

要实现多线程的特性,我们必须调用Thread类中的start()。那么既然Runnable不具备多线程特性,且我们是需要通过Thread来开辟新线程的,那么Runnable接口存在的意义是什么呢?

假设我们要创建一个类A,它需要具备多线程的特性,还需要继承另外一个类B。此时仅仅通过继承Thread就不能满足上述需求了,因为Java单继承的特性,如果继承了Thread类,类A就不能再继承类B。此时就需要将类A实现Runnable接口,重写run()并继承类B,然后通过Thread类实现该类的多线程特性。

因此,为了提高类的可扩展性,更推荐采用实现Runnable接口的方式实现多线程特性


 

如何处理线程的返回值

对于一个线程,它的执行进度我们是无法得知的,那么要如何才能在正确的时机获取线程的返回值呢?实现方式有3种:

①主线程等待法

这个方法有点蠢,就不进行分析了,下面贴个代码

public class CycleWait implements Runnable{
    private String value;//用于接收返回值

    @Override
    public void run() {
        try {
            Thread.currentThread().sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //线程先阻塞了5s后,才对value进行赋值
        value = "we have date now";
    }

    public static void main(String[] args) throws InterruptedException {
        CycleWait cw = new CycleWait();
        Thread t1 = new Thread(cw);
        t1.start();
        //如果cw.value为空,说明线程并未完成赋值,循环等待
        while (cw.value == null){
            Thread.currentThread().sleep(100);
        }
        System.out.println(cw.value);
    }
}

②join

join()方法的作用是使当前线程陷入WAITING,直到调用join的线程对象销毁,例如在A线程中调用了B线程的join()方法时,表示只有当B线程执行完毕时,A线程才能继续执行。

对于join方法,详细参考:...待填坑

public class CycleWait implements Runnable{
    private String value;//用于接收返回值

    @Override
    public void run() {
        try {
            Thread.currentThread().sleep(3000);
            //对返回值赋值
            value = "we have date now";
            //再阻塞1s
            Thread.currentThread().sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //线程先阻塞了5s后,才对value进行赋值
    }

    public static void main(String[] args) throws InterruptedException {
        CycleWait cw = new CycleWait();
        Thread t1 = new Thread(cw);
        t1.start();
        //如果cw.value为空,说明线程并未完成赋值,循环等待
        t1.join();
        System.out.println(cw.value);
    }
}

可以看到,我在run()方法中赋值语句后面又加了一行sleep(1000),其目的是用来模拟赋值的后续操作。这时我们应该意识到了,如果赋值行为是在run()方法的结尾那么问题不大,如果是在run方法执行中赋值,那么其效率甚至比第一个方法还要低,因为调用了t1.join,则主线程必须要等待线程t1进入销毁状态时才恢复执行。

所以结论是join还不够精准。

③通过Callable接口实现

我们可以通过Callable中的call方法获取返回值。可以在Callable接口的源码中观察到,它只有一个带泛型返回值的call()方法。(和Runnable接口长的挺像,方法一个有参一个无参)

public interface Callable<V> {  
    V call() throws Exception;
}

a. 通过FutureTask

FutureTask类继承自RunnableFuture接口,RunnableFuture接口又继承了Runnable接口,Future接口。

下面贴一下FutureTask的关键部分的源码:

可以看到FutureTask的两个构造方法,其实传入参数的都是一个Callable类型,第二个构造方法在内部也对Runnable接口进行了转换。

    public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;       // ensure visibility of callable
    }
   
    public FutureTask(Runnable runnable, V result) {
        this.callable = Executors.callable(runnable, result);
        this.state = NEW;       // ensure visibility of callable
    }

 

FutureTask通过get()方法来获取传入的Callable实例中call()方法中的返回值,它会等待call()方法有返回值了才执行。

可以看到第二个get()方法中可以接收一个timeout的参数,它可以指定在规定时间内若没有接收到返回值,则抛出一个TimeoutException。

public V get() throws InterruptedException, ExecutionException {
        int s = state;
        if (s <= COMPLETING)
            s = awaitDone(false, 0L);
        return report(s);
    }

    public V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException {
        if (unit == null)
            throw new NullPointerException();
        int s = state;
        if (s <= COMPLETING &&
            (s = awaitDone(true, unit.toNanos(timeout))) <= COMPLETING)
            throw new TimeoutException();
        return report(s);
    }

 

b. 通过线程池获取返回值

步骤和通过FutureTask差不太多,首先实例化一个线程池,然后调用该线程池执行传入的Callable实例并通过Future接收,之后的步骤都和上面一样了,需要注意的是线程池必须在用完之后关闭。

public class ThreadPoolDemo {
    public static void main(String[] args){
        ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
        Future<String> future = newCachedThreadPool.submit(new MyCallable());
        if(!future.isDone()){
            System.out.println("task has not finished please wait");
        }
        try {
            System.out.println(future.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }finally {
            newCachedThreadPool.shutdown();
        }
    }
}

关于线程池,可以参考:待填坑...


 

下面就是多线程的重中之重了

线程的六大状态

新建NEW

表示线程刚被创建,还未真正启动时的状态,我们执行Thread t = new Thread();时,线程t就处于一个NEW状态。

就绪RUNNABLE

表示线程已经在JVM中执行,它可能正在运行,也可能在就绪队列中排队等待系统给它分配CPU时间片。需要额外注意的是,JavaAPI是不能分辨该线程是处于正在运行还是处于排队等待的状态的。

我们执行t.start();时,线程就处于RUNNABLE状态了。

阻塞BLOCK

表示线程在等待获取排它锁Monitor lock,比如线程试图通过synchronized去获取某个锁,但是其他线程已经独占了,那么当前线程就会处于阻塞的状态。

无限等待WAITNG

表示正在等待其他线程采取某些操作,不会被CPU分配事件片,需要一直等待直到被其他线程显式地唤醒。常见的场景比如生产者消费者模式,生产者线程发现任务条件尚未满足,就让当前消费者线程WAITING,此时生产者线程去准备任务数据,然后通过类似notify等动作唤醒消费者线程。

比如在线程A中调用线程B的join(),线程A就会陷入WAITING状态,直到线程B执行完毕后唤醒线程A;

还有就是t1.wait(),不指定Timeout参数时,也会使t1陷入WAITING状态。

计时等待TIMED_WAIT

其进入条件和等待状态类似,但是调用的是存在超时条件的方法,比如wait或join等方法的指定超时版本,在一定时间后由系统自动唤醒。

比如调用t1.sleep(xxx ms),t1.wait(xxx ms)等,t1.join(xxx ms)。

终止TERMINATED

无论是意外退出还是正常执行结束,线程的使命已经完成,终止运行,且该状态是不可逆的。

 

这里抛出一个比较常见的问题:对一个线程调用两次start()方法会出现什么情况?

通过上述的线程六大状态的描述可以得知:我们对一个线程调用两次start(),在第二次调用的时候线程可能已经处于终止或非NEW状态,

从start的源码也看得出,如果调用对象得线程状态!=NEW,就会抛出IllegalThreadStateException,这是一种运行时异常,多次调用start会被认为是编程错误。

public synchronized void start() {     
        if (threadStatus != 0)
            throw new IllegalThreadStateException();
        
        ....
}

 


sleep和wait的区别

①sleep是Thread类中的方法,而wait是Object类中的方法。

②sleep()可以在任何位置使用,而wait()只能在synchronized块或synchronized方法中使用,因为释放锁的前提是获取锁

③最本质的区别:Thread.sleep只会让出CPU,不会导致锁行为的改变;Object.wait不仅会让出CPU,还会释放当前占有的锁资源


锁池和等待池

对于Java虚拟机中运行程序的每个对象来说,都有两个池:锁池EntryList,等待池WaitSet,而这两个池又与Object类的:wait(),notify(),notifyAll(),以及synchronized相关。

锁池

假设线程A已经拥有了某个对象的锁,而其他线程B,C想要调用这个对象的某个synchronized方法(或者块),由于B,C在进入对象的synchronized方法/块之前,必须要先获得该对象锁的拥有权,而恰好该对象的锁目前正在被线程A占用,此时线程B,C就会进入阻塞BLOCK状态,进入一个地方去等待锁的释放,这个地方便是该对象的锁池。

等待池

假设线程A调用了某个对象的wait方法,线程A就会释放该对象的锁,同时线程A就进入到了该对象的等待池中,也就是无限等待WAITING状态 ,进入到等待池中的线程不会去竞争该对象的锁。


notify和notifyAll的区别

notifyAll会让所有处于等待池中线程全部进入锁池去竞争锁的机会,也就是从WAITING状态转为BLOCK状态。

notify只会随机选取一个处于等待池中的线程进入锁池去竞争锁的机会。


yield

当调用Thread.yield函数时,会给当前线程调度器一个愿意让出CPU使用的暗示,但是调度器可能会忽略这个暗示,直观地说,调用yield方法让出CPU可能有效也可能无效。

yield并不会像wait一样让出当前线程占据的锁资源。


如何中断线程

在Java早期版本中,可以调用stop方法终止线程,但该方法已经被弃用。因为一个线程未在正常结束之前,被强制终止是很危险的,它可能会带着自己持有的锁永远休眠,迟迟不归还锁等。

那么又要满足不会强制终止线程,又要让线程能够死掉或是结束某种等待的状态,这时就得依靠interrupt方法了。interrupt方法可以用来请求终止线程,但具体是否中断取决于线程自身。

中断线程使用的场景

在某个子线程中,为了等待一些特定条件的到来,你调用了Thread.sleep(10000),预期线程睡了10s后自己醒来,但是如果这个特定的条件提前到来的话,就需要提前通知这个处于Sleep的线程。

线程通过调用子线程的join方法,阻塞自己以等待子线程结束,但是子线程在运行过程中发现自己无法在短时间内结束,于是就需要告诉主线程停止等待,这时候就需要中断。

 

当对一个线程调用interrupt方法时,线程的中断标志位置为true,这是每一个线程对象都具备的boolean属性。每个线程都应该不时地检查这个标志,以判断线程是否应该被终止。

我们可以通过调用isInterrupted方法判断该线程的中断状态是否被置位

while (!Thread.currentThread().isInterrupted()){
//Do something
}

但是,如果线程被阻塞,就无法检测中断状态,此时抛出一个InterruptedException。没有占用CPU运行的线程是不可能给自己的中断状态置位的。

如果在每次工作迭代后都调用sleep方法(或是其他让出CPU的方法),isInterrupted检测既没有必要也没有作用。

如果在一个线程中断的标识为true时调用sleep方法,它也不会进行休眠,反而会清除这一标识并抛出InterruptedException。因此,如果循环调用sleep,是不会检测中断状态的。


什么是线程死锁?如何避免死锁

如图所示,线程A持有资源1的锁,它若想执行完毕并释放资源1的锁则必须要获取到资源2的锁。

线程B持有资源2的锁,它若想执行完毕并释放资源2的锁则必须要获取到资源1的锁。

这两个线程都持有互相需要的锁资源,因此陷入了无休止的互相等待,这就是死锁。

下面是上述事例的Java代码,运行后发现线程A,线程B都陷入了无休止的互相等待状态——死锁状态。

public class DeadLockDemo {
    private static Object resource1 = new Object();
    private static Object resource2 = new Object();

    public static void main(String[] args) {
        new Thread(()->{
            synchronized (resource1){
                System.out.println(Thread.currentThread()+" get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread()+" wait resource2");
                synchronized (resource2){
                    System.out.println(Thread.currentThread()+" get resources2");
                }
            }
        },"线程A").start();

        new Thread(()->{
            synchronized (resource2){
                System.out.println(Thread.currentThread()+" get resource2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread()+" wait resource1");
                synchronized (resource1){
                    System.out.println(Thread.currentThread()+" get resources1");
                }
            }

        },"线程B").start();
    }

如何避免死锁

产生死锁必须具备以下四个条件:

  1. 互斥条件:该资源任意一个时刻只由一个线程占用。
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

那么只要破坏掉其中一条即可:

破坏互斥条件:这个无法破坏,因为该条件就是加锁的目的

破坏请求与保持条件:通过一次性申请所有的资源

破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,若申请不到,则主动释放它占有的资源

破坏循环等待条件

只需要将上述线程B的代码修改为如下,这样线程B在线程A释放resource1,resource2的锁之前,都无法获取锁,破坏了循环等待条件。

new Thread(()->{
            synchronized (resource1){
                System.out.println(Thread.currentThread()+" get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread()+" wait resource2");
                synchronized (resource2){
                    System.out.println(Thread.currentThread()+" get resources2");
                }
            }
        },"线程B").start();

也可以通过用完resource后就释放来避免死锁:

public class DeadLockDemo {
    private static Object resource1 = new Object();
    private static Object resource2 = new Object();

    public static void main(String[] args) {
        new Thread(()->{
            synchronized (resource1){
                System.out.println(Thread.currentThread()+" get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread() + " wait resource2");
            synchronized (resource2) {
                System.out.println(Thread.currentThread() + " get resources2");
            }
        },"线程A").start();

        new Thread(()->{
            synchronized (resource2) {
                System.out.println(Thread.currentThread() + " get resource2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread() + " wait resource1");
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + " get resources1");
            }
        },"线程B").start();
    }

}

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值