并发编程线程基础

1.线程和进程

  • 线程是进程中的一个实体,线程本身是不会独立存在的,线程是进程的一个执行路径
  • 进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,一个进程中至少有一个线程,进程中的多个线程共享进程的资源(堆和方法区资源)
资源分配
  • 操作系统在分配资源时是把资源分配给进程的
  • 但是CPU资源比较特殊,它是被分配到线程的,因为真正要占用CPU运行的是线程,所以也说线程是CPU分配的基本单位

2.线程的创建与运行

Java中有三种线程创建方式,分别为Runnable接口的run方法,继承Thread类并重写run的方法,使用FutureTask方式。

(1) 继承Thread类方式实现
public class ThreadTest {
    public static class myThread extends Thread {
        @Override
        public void run() {
            System.out.println("I am a child thread");
        }
    }
    public static void main(String[] args) {
        MyThread thread = enw MyThread();
        thread.start();
    }
}
  • 使用继承的好处是,在run()方法内获取到当前线程直接使用this就可以了,无须使用Thread.currentThread()方法;可以在子类里面添加成员变量,通过set方法设置参数或者通过构造函数进行传递
  • 不好的地方是Java不支持多继承,如果继承了Thread类,那么就不能再继承其他类。另外任务与代码没有分离,当多个线程执行一样的任务时需要多份任务代码,而Runnable没有这个限制。
(2) 实现Runnable接口的run方法方式实现
public static class RunnableTask implements Runnable {
    @Override
    public void run() {
        System.out.println("I am a child thread");
    }
}
public static void main(String[] args) throws InterruptedException {
    Runnable task = new RunnableTask();
    new Thread(task).start();
    new Thread(task).start();
}
  • 上述代码中,两个线程共用一个task代码逻辑,如果需要,可以给RunnableTask添加参数进行任务区分。
  • 另外,RunnbaleTask可以继承其他类
  • 但是只能使用主线程里面被声明为final的变量
(3) 使用FutureTask的方式实现
public static class CallerTask implements Callable<String> {
    @Override
    public String call() throws Exception {
        return "hello";
    }
}
public static void main(String[] args) throws InterruptedException {
    FutureTask<String> futureTask = new FutureTask<>(new CallerTask());
    new Thread(futureTask).start();
    try {
        String result = futureTask.get();
        System.out.println(result);
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
}
  • 前两种方式都无法拿到任务的返回结果,但是Futuretask方式可以

3. 线程通知与等待

下面的所有方法都是Object类中的方法

(1) wait()函数
  • 当一个线程调用一个共享变量的wait()方法时,该调用线程会被阻塞挂起,知道发生下面几件事情之一才返回

    • 其它线程调用了该共享对象的notify()方法或者notifyAll()方法
    • 其它线程调用了该线程的interrupt()方法,该方法抛出InterruptedException异常返回

    注意——如果调用wait()方法的线程没有事先获取该对象的监视器锁,则调用wait()方法时调用线程会抛出IllegalMonitorStateException异常

  • 一个线程如何获取一个共享变量的监视器锁

    • 执行synchronized同步代码块时,使用该共享变量作为参数
    synchronized (共享变量) {
        // do something
    }
    
    • 调用该共享变量的方法,并且该方法使用了synchronized修饰
    synchronized void add (int a, int b) {
        // do something
    }
    
    虚假唤醒

    一个线程可以从挂起状态变为可运行状态,即使该线程没有被其他线程调用notify()notifyAll()方法进行通知,或者被中断,或者等待超时

    解决办法

    不停地去测试该线程被唤醒的条件是否满足,不满足则继续等待

    synchronized (obj) {
        while (条件不满足) {
            obj.wait();
        }
    }
    
  • 当前线程调用共享变量的wait()方法后,只会释放当前共享变量上的锁,如果当前线程还持有其他共享变量的锁,则这些锁是不会被释放的。

  • 当一个线程调用共享对象的wait()方法被阻塞挂起后,如果其他线程中断了该线程,则该线程会抛出InterruptedException异常并返回。

(2) wait(long timeout)函数
  • 如果一个线程调用共享对象的该方法挂起后,没有在指定时间的timeout ms时间内被其它线程调用该共享变量的notify()或者notifyAll()方法唤醒,那么该函数还是会因为超时而返回。
  • 如果在调用该函数时,传递了一个负的timeout则会抛出IllegalArgumentException异常。
(3) wait(long timeout, int nanos)函数
  • 在其内部调用的是wait(long timeout)函数,如下代码只有在nanos > 0时才使参数timeout递增1.
public final void wait(long timeout, int nanos) throws InterruptedException {
    if (timeout < 0) {
        throw new InterruptedException("timeout value is negative");
    }
    if (nanos < 0 || nanos > 999999) {
        throw new InterruptedException("nanosencond timeout value out of range");
    }
    if (nanos > 0) {
        timeout ++;
    }
    wait(timeout);
}
(4) notify()函数
  • 一个线程调用共享对象的notify()方法后,会唤醒一个在该共享变量上调用wait系列方法后被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的。
  • 被唤醒的线程不能马上从wait方法返回并继续执行,它必须在获取了共享对象的监视器锁后才可以返回,也就是唤醒它的线程释放了共享变量上的监视器锁,被唤醒的线程也不一定会获取到共享对象的监视器锁,这是因为该线程还需要和其他线程一起竞争该锁,只有该线程竞争到了共享变量的监视器锁后才可以继续执行。
  • 类似wait系列方法,只有当前线程获取到了共享变量的监视器锁后,才可以调用共享变量的notify()方法,否则会抛出InterruptedException异常。
(5) notifyAll()函数
  • 会唤醒所有在该共享变量上由于调用wait系列方法而被挂起的线程
  • 如果调用notifyAll()方法后一个线程调用了该共享变量的wait()方法而被放入阻塞集合,则该线程是不会被唤醒的

4. 等待线程执行终止的join方法

  • join方法是Thread类直接提供的
  • join是无参且返回值为void的方法
  • 当某个线程的join方法被调用后,当前调用该方法的线程会被阻塞,等待其结束再执行之后的语句。若在阻塞的过程中其他线程调用了当前线程的interrupted()方法中断了该线程,该线程则会抛出InterruptedException异常而返回

5. 让线程睡眠的sleep方法

  • Thread类中有一个静态的sleep方法,当一个执行中的线程调用了Thread的sleep方法后,调用线程会暂时让出指定时间的执行权,也就是在这期间不参与CPU的调度,但是该线程所拥有的监视器资源,比如锁还是持有不让出的
  • 指定的睡眠时间到了之后,该函数会正常返回,线程就处于就绪状态,然后参与CPU调度,获取到CPU资源后就可以继续运行了。
  • 如果在睡眠期间其他线程调用了该线程的interrupted()方法中断了该线程,则该线程会在调用sleep方法的地方抛出InterruptedException异常而返回。

6. 让出CPU执行权的yield方法

  • Thread类中有一个静态的yield方法,当一个线程调用yield方法后,实际就是在暗示线程调度器当前线程请求让出自己的CPU使用,但是线程调度器可以无条件忽略这个暗示。
sleep与yield区别
  • 当线程调用sleep方法时调用线程会被阻塞挂起指定时间,在这期间线程调度器不会去调度该线程。
  • 而调用yield方法时,线程只是让出自己剩余的时间片,并没有被阻塞挂起,而是处于就绪状态,线程调度器下一次调度时就有可能调度到当前线程执行。

7. 线程中断

Java中的线程中断是一种线程间的协作模式,通过设置线程的中断标志并不能直接终止该线程的执行,而是被中断的线程根据中断状态自行处理。

(1) void interrupt()方法
  • 中断线程,例如,当线程A运行时,线程B可以调用线程A的interrupt()方法来设置线程A的中断标志为true并立即返回。设置标志仅仅是设置标志,线程A实际并没有被中断,它会继续往下执行。如果线程A因为调用了wait系列函数、join方法或者sleep方法而被阻塞挂起,这时候若线程B调用线程A的interrupt()方法,线程A会在调用这些方法的地方抛出InterruptedException异常而返回。
(2) boolean isInterrupted()方法
  • 检测当前线程是否被中断,如果是返回true,否则返回false。不清楚中断标志。
(3) boolean interrupted()方法
  • 检测当前线程是否被中断,如果是返回true,否则返回false。
  • 与isInterrupted()方法不同的是,该方法如果发现当前线程被中断,则会清除中断标志,并且该方法是static方法,可以通过Thread类直接调用。

8. 线程死锁

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去。

产生死锁的四个条件
  • 互斥条件。指线程对已经获取到的资源进行排他性使用,即该资源同时只有一个线程占用。如果此时还有其它线程请求获取该资源,则请求者只能等待,直到占有资源的线程释放该资源。
  • 请求并持有条件。指一个线程已经持有了至少一个资源,但又提出了新的资源请求,而新资源已被其它线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己已经获取到的资源。
  • 不可剥夺条件。指线程获取到的资源在自己使用完之前不能被其它线程抢占,只有在自己使用完毕后才由自己释放该资源。
  • 环路等待条件。指在发生死锁时,不然存在一个线程——资源的环形链,即线程集合{T0,T1,T2,…,Tn}中的T0正在等待一个T1占用的资源,T1正在等待T2占用的资源,…Tn正在等待已被T0占用的资源。

9. 守护线程和用户线程

  • Java中的线程分为两类,分别为daemon线程(守护线程)和user线程(用户线程)。
  • 守护线程和用户线程的区别之一就是,当最后一个非守护线程结束时,JVM会正常退出,而不管当前是否有守护线程,也就是说,守护线程是否结束并不影响JVM的退出。言外之意就是,只要有一个用户线程还没结束,正常情况下JVM就不会退出。
如何创建守护线程
public static void mian(String[] args) {
    Thread daemonThread = new Thread(new Runnable() {
       public void run() {
           
       } 
    });
    
    // 设置为守护线程
    daemonThread.setDaemon(true);
    daemonThread.start();
}

10. ThreadLocal

问题:多线程访问同一个共享变量时特别容易出现并发问题,特别是在多个线程需要对一个共享变量进行写入时。为了保证线程安全,一般使用者在访问共享变量时需要进行适当的同步。同步的措施一般是加锁,那么有没有另一种方式可以做到?

方案:ThreadLocal是JDK包提供的,它提供了线程本地变量,也就是如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。但股东个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

HardyZhan

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值