java多线程:线程基础

文章目录

      • 线程创建
      • 线程通知与等待
        • 虚假唤醒
        • wait()、wait(long timeout)、wait(long timeout,int nanous)函数
        • notify()、notifyAll()函数
      • 等待线程执行终止的join方法
      • 让线程睡眠的sleep方法
        • Sleep方法与wait方法区别
      • 让出CPU执行权的yield方法
        • sleep()与yield()区别
      • 线程中断
      • 线程死锁
        • 为什么会产生死锁??
        • 如何避免线程死锁?
      • 守护线程与用户线程
      • 线程组和线程优先级
        • 线程组
        • 线程的优先级
        • 线程组的常用方法及数据结构
      • ThreadLocal

线程创建

线程创建有三种方式:

  • 实现Runnable接口的run方法
  • 继承Thread类并重写run方法
  • 实现Callable接口,使用FutureTask方式

继承Thread类并重写run方法

package threadtxt;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class ThreadTest {
    /***
     *  继承Thread 类并重写run方法
     */
    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 myThread = new MyThread();
        //        启动线程
                myThread.start();
    }
}

注意点:

当线程创建完thread对象后该线程并没有启动,直到调用了start方法才真正的启动了线程。调用start方法后线程没有马上执行,而是进入了就绪状态,这个时候已经获取了除CPU资源外的其他资源,等待获取CPU资源后才会真正处于运行状态。run方法执行完毕之后,该线程就处于终止状态。

具体可以参考 : Java多线程:概念

实现Runnable接口的run方法

package threadtxt;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThreadTest {
    /***
     * 实现Runnable接口
      */
    public static class RunableTask implements Runnable{
        @Override
        public void run() {
            System.out.println("i am a child thread");
        }
    }
    public static void main(String[] args) {
        RunableTask runableTask = new RunableTask();
        new Thread(runableTask).start();
        new Thread(runableTask).start();
    }
}

实现Callable接口,使用FutureTask方式

package threadtxt;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThreadTest {
    /***
     * 使用FutureTask方式实现
     */
    public static class CallerTask implements Callable<String>{
        @Override
        public String call() throws Exception {
            return "hello";
        }
    }

    public static void main(String[] args) {

//        创建异步任务
        FutureTask<String> futureTask = new FutureTask<>(new CallerTask());
//       创建线程并启动
        new Thread(futureTask).start();

        try {
//            等待任务执行完毕,返回结果
            String rtn = futureTask.get();
            System.out.println(rtn);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }


    }
}

知识点

在程序里面调用start方法后,虚拟机会先为我们创建一个线程,然后等到这个线程第一次得到时间片时再调用run()方法。但是不可多次调用start方法,在第一次调用start方法后,再次调用start方法会抛出异常,因为start方法中在使用前会先判断threadStatus变量是否为0,如果不为0则抛出异常

线程通知与等待

Java多线程的等待/通知机制是基于Object类的wait()方法和notify()、notifyAll()方法来实现。

notify()方法会随机唤醒一个正在等待的线程,而notifyAll()会叫醒所有正在等待的线程

线程调用wait方法时,该线程就会被阻塞挂起,直到发生下面几件才会返回:

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

知识点

调用wait方法的线程没有事先获取该对象的监视器锁,则调用wait方法时,会抛出illegalMonitorStateException异常

问题1:一个线程如何才能获取一个共享变量的监视器锁?

(1) 执行synchronized同步代码块时,使用该共享变量作为参数

synchronized(共享变量){
    //业务逻辑
}

(2)调用该共享变量的方法,并且该犯法使用了synchronized修饰

synchronized void add(int a,int b){
    //业务逻辑
}

虚假唤醒

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

问题1:怎么防止虚假唤醒?

​ 不停的去测试该线程被唤醒的条件是否满足,不满足则继续等待,即是在一个循环中调用wait方法进行防范。退出循环的条件是满足了唤醒该线程的条件。

synchronized(obj){
    while(条件是否满足){
        obj.wait();
    }
}

以上代码中的while()循环,则不能用if代替,如果使用if则只会判断一次,当下次条件不满足时 ,线程也会唤醒。所以等待应该出现在循环当中

一般并发线程操作的步骤:

  • 判断等待
  • 业务操作
  • 通知

知识点

当前线程调用共享变量wait方法后只会释放当前变量上的锁,如果当前线程还持有其他共享变量锁,则这些锁是不会被释放的。

wait()、wait(long timeout)、wait(long timeout,int nanous)函数

wait(long timeout)相比wait()方法多了一个超时参数,如果一个线程调用共享对象的该方法挂起后,没有在指定的timeout ms 时间内被其他线程调用该共享变量的notify()或者notifyAll()方法唤醒,那么该函数还是会因为超时而返回。

知识点

wait(0)方法和wait()方法效果一样,因为在wait方法内部就是调用了wait(0)。需要注意的是,如果在调用函数时,传递了一个负的timeout则会抛出illegalArgumentException异常。

notify()、notifyAll()函数

线程调用notify()方法后,会唤醒一个在该共享变量上调用wait系列方法后被挂起的线程。

知识点

  • 一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的。
  • 被唤醒的线程不能马上从wait方法返回并继续执行,它必须在获取了共享对象的监视器锁后才可以返回,也就是唤醒它的线程释放了共享变量上的监视器锁后,被唤醒的线程也不一定会获取到该共享对象的监视器锁,因为其他线程会和当前线程一起竞争该锁,只有该线程竞争到了共享变量的监视器锁后才可以继续执行。
  • 只有当前线程获取到了共享变量的监视器锁后,才可以调用该共享变量的notify方法,否会抛出illegalMontitorStateException异常。

notifyAll方法则会唤醒所有在该共享变量上由于调用wait系列方法而被挂起的线程。

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

怎么快速理解下线程中join()方法???

想象一下,你现在在排队买奶茶,快要到你的时候,突然来个非常漂亮的妹子说:帅哥,可以让我先买吗?(这个时候想,长得还可以,让你先买吧,等你买完就到我了,说不定让你先买,还能留个微信号,交个朋友),然后你说:可以啊,于是妹子站在你前面买奶茶,可是这个时候,她的七大姑八大姨都来了,都排在那个妹子前面,你从前面的第三位,直接变成第N位。这个时候只能感叹,早知道我就先买了。线程中的join方法就是这个道理了。让其他的线程先执行完毕后,然后自己在执行操作。

举个栗子

package threadtxt2;

import java.util.concurrent.TimeUnit;
public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        Thread tA = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(" child tA over!!! ");
        });
        Thread tB = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(" child tB over!!! ");
        });
        tA.start();
        tB.start();
        System.out.println("wait all child");
        //等待子线程执行完毕,返回
        tA.join();
        tB.join();
        System.out.println("over all child");
    }

}

返回结果

wait all child
child tB over!!!
child tA over!!!
over all child

知识点

join()方法是Thread 类的一个实例方法。它的作用是让当前线程陷入“等待”状态,等待join的这个线程执行完成后,再继续执行当前线程。

有时候,主线程创建并启动了子线程,如果子线程需要进行大量的耗时运算,主线程往往将早于子线程结束之前结束。

如果主线程想等待子线程执行完毕后,获得子线程中的处理完的某个数据,就要用到join方法

注意点

join()方法由两个重载方法,一个是join(long),一个是join(long,int)。

实际上,通过源码发现,join()方法及其重载方法底层都是利用了wait(long timeout)这个方法

对于join(long,int),通过查看源码(JDK1.8)发现,底层并没有精确到纳秒,而是对第二个参数做了简单的判断和处理

让线程睡眠的sleep方法

当一个执行中的线程调用了Thread的sleep方法后,调用线程会暂时让出指定时间的执行权,也就是在这期间不参与CPU的调度,但是该线程所拥有的监视器资源,比如锁还是持有状态。

指定的睡眠时间到了后该函数就会正常返回,线程处于就绪状态,然后参与CPU调度,获取CPU资源后就可以继续运行了。

知识点

在调用sleep方法的线程,睡眠期间其他线程调用了interrupt()方法中断了该线程,则该线程会在调用sleep方法的地方抛出InterruptedException异常返回

Sleep方法与wait方法区别

  • wait方法可以指定等待时间,也可以不指定。Sleep方法必须指定
  • wait方法释放CPU资源,同时释放锁。sleep方法释放cpu资源,但是不释放锁。所以容易死锁。
  • wait方法必须方法在同步块或同步方法中,而sleep可以放在任意位置

让出CPU执行权的yield方法

当一个线程调用yield方法时,当前线程会让出CPU使用权,然后处于就绪状态,线程调度器会从线程就绪队列里面获取一个线程优先级最高的线程,当然也有可能会调度到刚刚让出CPU的那个线程。

sleep()与yield()区别

当前线程调用sleep方法时调用线程会被阻塞挂起指定的时间,在这期间线程调度器不会去调度其他线程。

调用yield方法时,线程只是让出自己剩余的时间片,并没有挂起,而是出于就绪状态,线程调度器下一次调度时就有可能调度到当前线程执行

线程中断

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

Thread类里提供的关于线程中断的几个方法:

  • Thread.interrupt() : 中断线程。这里的中断线程并不会立即停止线程,而是设置线程的中断状态为true(默认为false);
  • Thread.interrupted(): 测试当前线程是否被中断,线程的中断状态受这个方法的影响,意思是调用一次使线程中断设置为true,连续调用两次会使得这个线程的中断状态重新转为false
  • Thread.isInterrupted() : 测试当前线程是否被中断,与上面方法不同的是调用这个方法并不会影响线程的中断状态。

在线程的中断机制里,当其他线程通知需要被中断的线程后,线程中断的状态被设置为true,但是具体被要求中断的线程要怎么处理,完全由被中断线程自己而定,可以在适合的实际处理中中断请求,也可以完全不处理继续执行下去。

线程死锁

死锁:指两个或者两个以上的线程在执行过程中,因争夺资源而造成的相互等待的现象。在无外力作用的情况下,这些线程会一直相互等待而无法继续运作下去。
java多线程:线程基础_第1张图片

为什么会产生死锁??

死锁的产生必须满足以下四个条件:

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

如何避免线程死锁?

  • 对资源进行有序的分配,让获取资源有先后顺序

守护线程与用户线程

Java中的线程分为两类,分别为daemon线程和user线程。

知识点

JVM的退出和用户线程有关,和守护线程无关。只要有一个用户线程没有结束,正常情况下JVM就不会退出。

守护线程默认的优先级比较低。一个线程默认是非守护线程,可以通过Thread类的setDaemon(boolean on )来设置

线程组和线程优先级

线程组

java中用ThreadGroup来表示线程组,我们可以使用线程组对线程进行批量控制。

每个Thread必然存在于一个ThreadGroup中,Thread不能独立于ThreadGroup存在。

ThreadGroup管理着它下面的Thread,ThreadGroup是一个标准的向下引用的树状结构。这样设计的原因是防止上级线程被下级线程引用而无法有效地被GC回收。

线程的优先级

Java中线程优先级可以指定,范围是1~10。但是并不是所有操作系统都支持10级优先级的划分,java只是给操作系统一个优先级参考值,线程最终在操作系统的优先级还是由操作系统决定。

知识点

java默认的线程优先级为5,线程的执行顺序有调度程序来决定,线程的优先级会在线程调用前设定。通常情况下,高优先级的线程比低优先级有更高的几率得到执行。我们使用方法Thread类的setPriority()方法来设定线程的优先级

线程组的常用方法及数据结构

获取当前的线程组名字

Thread.currentThread().getThreadGroup().getName()

复制线程组

//复制一个线程数组到一个线程组
Thread[] threads = new Thread[threadGroup.activeCount()];
ThreadGroup threadGroup = new ThreadGroup();
threadGroup.enumerate(threads);

线程组统一异常处理

public static void main(String[] args){
    ThreadGroup threadGroup1 = new ThreadGroup("group1") {
        // 继承ThreadGroup并重新定义以下⽅法
        // 在线程成员抛出unchecked exception
        // 会执⾏此⽅法
        public void uncaughtException(Thread t, Throwable e) {
            System.out.println(t.getName() + ": " + e.getMessage());
        }
    };
    // 这个线程是threadGroup1的⼀员
    Thread thread1 = new Thread(threadGroup1, new Runnable() {
        public void run() {
            // 抛出unchecked异常
            throw new RuntimeException("测试异常");
        }
    });
    thread1.start();
}

线程组的数据结构

线程组还可以包含其他的线程组,不仅仅是线程。

public class ThreadGroup implements Thread.UncaughtExceptionHandler {
    private final ThreadGroup parent; // ⽗亲ThreadGroup
    String name; // ThreadGroupr 的名称
    int maxPriority; // 线程最⼤优先级
    boolean destroyed; // 是否被销毁
    boolean daemon; // 是否守护线程
    boolean vmAllowSuspension; // 是否可以中断
    int nUnstartedThreads = 0; // 还未启动的线程
    int nthreads; // ThreadGroup中线程数⽬
    Thread threads[]; // ThreadGroup中的线程
    int ngroups; // 线程组数⽬
    ThreadGroup groups[]; // 线程组数组
}

构造函数:

// 私有构造函数
private ThreadGroup() {
    this.name = "system";
    this.maxPriority = Thread.MAX_PRIORITY;
    this.parent = null;
}
// 默认是以当前ThreadGroup传⼊作为parent ThreadGroup,新线程组的⽗线程组是⽬前正在运⾏线
public ThreadGroup(String name) {
    this(Thread.currentThread().getThreadGroup(), name);
}
// 构造函数
public ThreadGroup(ThreadGroup parent, String name) {
    this(checkParentAccess(parent), parent, name);
}
// 私有构造函数,主要的构造函数
private ThreadGroup(Void unused, ThreadGroup parent, String name) {
    this.name = name;
    this.maxPriority = parent.maxPriority;
    this.daemon = parent.daemon;
    this.vmAllowSuspension = parent.vmAllowSuspension;
    this.parent = parent;
    parent.add(this);
}

第三个构造函数⾥调⽤了 checkParentAccess ⽅法,这⾥看看这个⽅法的源码:

// 检查parent ThreadGroup
private static Void checkParentAccess(ThreadGroup parent) {
    parent.checkAccess();
    return null;
}
// 判断当前运⾏的线程是否具有修改线程组的权限
public final void checkAccess() {
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        security.checkAccess(this);
    }
}

知识点

这⾥涉及到 SecurityManager 这个类,它是Java的安全管理器,它允许应⽤
程序在执⾏⼀个可能不安全或敏感的操作前确定该操作是什么,以及是否是
在允许执⾏该操作的安全上下⽂中执⾏它。应⽤程序可以允许或不允许该操
作。
⽐如引⼊了第三⽅类库,但是并不能保证它的安全性。
其实Thread类也有⼀个checkAccess()⽅法,不过是⽤来当前运⾏的线程是
否有权限修改被调⽤的这个线程实例。(Determines if the currently running
thread has permission to modify this thread.)

总结来说,线程组是⼀个树状的结构,每个线程组下⾯可以有多个线程或者线程
组。线程组可以起到统⼀控制线程的优先级和检查线程的权限的作⽤

ThreadLocal

多线程访问同一个共享变量时特别容易出现并发问题,特别是在多个线程需要对一个共享变量写入时。为了保证线程安全,一般使用者在访问共享变量时需要进行适当的同步。同步的措施是加锁,这就需要使用者对锁有一定的了解。
java多线程:线程基础_第2张图片

问题:有没有一种方式可以做到,当创建一个变量后,每个线程对其进行访问的时候访问的是自己的变量?

ThreadLocal可以做这个事情,它提供了线程本地变量,也就是如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全问题。

java多线程:线程基础_第3张图片
知识点

如果开发者希望将类的某个静态变量(UserID或者transaction ID )与线程状态关联,则可以考虑使用ThreadLocal。最常见的ThreadLocal使用场景为用来解决数据库连接、Session管理等,数据库连接和Session管理涉及多个复杂对象的初始化和关闭,如果在每个线程中声明一些私有变量来进行操作,那这个线程就变得不那么轻量了,需要频繁的创建和关闭连接。

你可能感兴趣的:(Java专栏)