Java线程

1、进程和线程的区别

  • 进程是操作系统管理的基本运行单元。360浏览器是一个进程、WPS也是一个进程,正在操作系统中运行的".exe"都可以理解为一个进程
  • 线程进程中独立运行的子任务就是一个线程。像QQ.exe运行的时候就有很多子任务在运行,比如聊天线程、好友视频线程、下载文件线程等等

2、创建线程

public class Test {
    public static void main(String[] args) {
        //第一种创建线程的方式
        MyThread mt = new MyThread();
        mt.start();//注意要调用start方法
        
        //第二种创新线程的方式
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Runnable run()!");
            }
        });
        thread.start();//注意这里还是要调用start方法
    }

    static class MyThread extends Thread{
        @Override
        public void run() {
          System.out.println("MyThread run()!");
        }
    }
}

注意区分run方法和start方法,run方法里是线程将要执行的程序,所以子类必须要去重新,当然你也可以不覆盖,这样线程也就什么也不做。

而start方法才是真正启动线程,调用了start()后线程才调用run()方法。

调用start()方法的顺序不代表线程启动的顺序,线程启动顺序具有不确定性。 

 

线程状态

虚拟机中的线程状态有六种,定义在Thread.State中:

1、新建状态NEW

new了但是没有启动的线程的状态。比如"Thread t = new Thread()",t就是一个处于NEW状态的线程

2、可运行状态RUNNABLE

new出来线程,调用start()方法即处于RUNNABLE状态了。处于RUNNABLE状态的线程可能正在Java虚拟机中运行,也可能正在等待处理器的资源,因为一个线程必须获得CPU的资源后,才可以运行其run()方法中的内容,否则排队等待

3、阻塞BLOCKED

如果某一线程正在等待监视器锁,以便进入一个同步的块/方法,那么这个线程的状态就是阻塞BLOCKED

4、等待WAITING

某一线程因为调用不带超时的Object的wait()方法、不带超时的Thread的join()方法、LockSupport的park()方法,就会处于等待WAITING状态

5、超时等待TIMED_WAITING

某一线程因为调用带有指定正等待时间的Object的wait()方法、Thread的join()方法、Thread的sleep()方法、LockSupport的parkNanos()方法、LockSupport的parkUntil()方法,就会处于超时等待TIMED_WAITING状态

6、终止状态TERMINATED

线程调用终止或者run()方法执行结束后,线程即处于终止状态。处于终止状态的线程不具备继续运行的能力

Thread类中的实例方法 

1、start()

start()方法的作用讲得直白点就是通知"线程规划器",此线程可以运行了,正在等待CPU调用线程对象得run()方法,产生一个异步执行的效果。通过start()方法产生得到结论,先看下代码:

2、run()

线程开始执行,虚拟机调用的是线程run()方法中的内容。稍微改一下之前的例子看一下:

3、isAlive()

测试线程是否处于活动状态,只要线程启动且没有终止,方法返回的就是true。看一下例子:

4、getId()

这个方法比较简单,就不写例子了。在一个Java应用中,有一个long型的全局唯一的线程ID生成器threadSeqNumber,每new出来一个线程都会把这个自增一次,并赋予线程的tid属性,这个是Thread自己做的,用户无法执行一个线程的Id。

5、getName()

这个方法也比较简单,也不写例子了。我们new一个线程的时候,可以指定该线程的名字,也可以不指定。如果指定,那么线程的名字就是我们自己指定的,getName()返回的也是开发者指定的线程的名字;如果不指定,那么Thread中有一个int型全局唯一的线程初始号生成器threadInitNum,Java先把threadInitNum自增,然后以"Thread-threadInitNum"的方式来命名新生成的线程

6、getPriority()和setPriority(int newPriority)

这两个方法用于获取和设置线程的优先级,优先级高的CPU得到的CPU资源比较多,设置优先级有助于帮"线程规划器"确定下一次选择哪一个线程优先执行。换句话说,两个在等待CPU的线程,优先级高的线程越容易被CU选择执行

线程默认优先级为5,如果不手动指定,那么线程优先级具有继承性,比如线程A启动线程B,那么线程B的优先级和线程A的优先级相同CPU会尽量将执行资源让给优先级比较高的线程

7、isDaeMon、setDaemon(boolean on)

讲解两个方法前,首先要知道理解一个概念。Java中有两种线程,一种是用户线程,一种是守护线程。守护线程是一种特殊的线程,它的作用是为其他线程的运行提供便利的服务,最典型的应用便是GC线程。如果进程中不存在非守护线程了,那么守护线程自动销毁,因为没有存在的必要,为别人服务,结果服务的对象都没了,当然就销毁了。

关于守护线程,有一个细节注意下,setDaemon(true)必须在线程start()之前

8、interrupt()

这是一个有点误导性的名字,实际上Thread类的interrupt()方法无法中断线程。看一下例子:

interrupt()方法的作用实际上是:在线程受到阻塞时抛出一个中断信号,这样线程就得以退出阻塞状态。换句话说,没有被阻塞的线程,调用interrupt()方法是不起作用的。关于这个会在之后讲中断机制的时候,专门写一篇文章讲解。

9、isInterrupted()

测试线程是否已经中断,但不清除状态标识。这个和interrupt()方法一样,在后面讲中断机制的文章中专门会讲到。

10、join()

讲解join()方法之前请确保对于wait()/notify()/notifyAll()机制已熟练掌握

join()方法的作用是等待线程销毁。join()方法反应的是一个很现实的问题,比如main线程的执行时间是1s,子线程的执行时间是10s,但是主线程依赖子线程执行完的结果,这时怎么办?可以像生产者/消费者模型一样,搞一个缓冲区,子线程执行完把数据放在缓冲区中,通知main线程,main线程去拿,这样就不会浪费main线程的时间了。另外一种方法,就是join()了。先看一下例子:

join()方法的一个重点是要区分出和sleep()方法的区别。join(2000)也是可以的,表示调用join()方法所在的线程最多等待2000ms,两者的区别在于:

sleep(2000)不释放锁,join(2000)释放锁,因为join()方法内部使用的是wait(),因此会释放锁。看一下join(2000)的源码就知道了,join()其实和join(2000)一样,无非是join(0)而已:

 

 Thread类中的静态方法

1、currentThread()

currentThread()方法返回的是对当前正在执行线程对象的引用。看一个重要的例子,然后得出结论:

2、sleep(long millis)

sleep(long millis)方法的作用是在指定的毫秒内让当前"正在执行的线程"休眠(暂停执行)。这个"正在执行的线程"是关键,指的是Thread.currentThread()返回的线程。根据JDK API的说法,"该线程不丢失任何监视器的所属权",直白点讲,就是不让出CPU资源。CPU还在执行当前线程run()方法中的代码,无非执行的内容是"睡觉"而已。看一下例子:

3、yield()

暂停当前执行的线程对象,并执行其他线程。这个暂停是会放弃CPU资源的,并且放弃CPU的时间不确定,有可能刚放弃,就获得CPU资源了,也有可能放弃好一会儿,才会被CPU执行。看一下例子:

4、interrupted()

测试当前线程是否已经中断,执行后具有将状态标识清除为false的功能。换句话说,如果连续两次调用该方法,那么返回的必定是false:

 

synchronized块和synchronized方法

synchronized取得的锁都是对象锁,而不是把一段代码或方法(函数)当作锁,哪个线程先执行带synchronized的方法,哪个线程就持有该方法所属对象的锁,其他线程都只能呈等待状态。但是这有个前提:多个线程访问的必须是同一个对象如果多个线程访问的是多个对象,那么Java虚拟机就会创建多个锁。既然多个线程持有的是不同的锁,自然不会受到"等待释放锁"这一行为的制约.

 当一个线程执行的代码出现异常时,其所持有的锁会自动释放

1、A线程持有Object对象的Lock锁,B线程可以以异步方式调用Object对象中的非synchronized类型的方法

2、A线程持有Object对象的Lock锁,B线程如果在这时调用Object对象中的synchronized类型的方法则需要等待,也就是同步

1、当A线程访问对象的synchronized代码块的时候,B线程依然可以访问对象方法中其余非synchronized块的部分

2、当A线程进入对象的synchronized代码块的时候,B线程如果要访问这段synchronized块,那么访问将会被阻塞

 

同一个对象,不同的线程,无论是synchronized块和synchronized方法,各线程都需要排队等待

同一个对象,不同方法的synchronized块互斥,会出现阻塞等待。

不同对象(可以new多个对象),同一个线程,不会被阻塞。

 

同步静态方法

synchronized还可以应用在静态方法上,如果这么写,则代表的是对当前.java文件对应的Class类加锁。静态同步方法和非静态同步方法持有的是不同的锁,前者是类锁,后者是对象锁

所谓类锁,举个再具体的例子。假如一个类中有一个静态同步方法A,new出了两个类的实例B和实例C,线程D持有实例B,线程E持有实例C,只要线程D调用了A方法,那么线程E调用A方法必须等待线程D执行完A方法,尽管两个线程持有的是不同的对象。

 

volatile关键字

直接先举一个例子:

public class MyThread28 extends Thread {
    private boolean isRunning = true;

    public boolean isRunning() {
        return isRunning;
    }

    public void setRunning(boolean isRunning) {
        this.isRunning = isRunning;
    }

    public void run() {
        System.out.println("进入run了");
        while (isRunning == true) {
        }
        System.out.println("线程被停止了");
    }

    public static void main(String[] args) {
        try {
            MyThread28 mt = new MyThread28();
            mt.start();
            Thread.sleep(1000);
            mt.setRunning(false);
            System.out.println("已赋值为false");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

看一下运行结果:

进入run了
已赋值为false

 也许这个结果有点奇怪,明明isRunning已经设置为false了, 线程还没停止呢?

这就要从Java内存模型(JMM)说起,JMM,Java中有一块主内存,不同的线程有自己的工作内存,同一个变量值在主内存中有一份,如果线程用到了这个变量的话,自己的工作内存中有一份一模一样的拷贝。每次进入线程从主内存中拿到变量值,每次执行完线程将变量从工作内存同步回主内存中。

出现打印结果现象的原因就是主内存和工作内存中数据的不同步造成的。因为执行run()方法的时候拿到一个主内存isRunning的拷贝,而设置isRunning是在main函数中做的,换句话说 ,设置的isRunning设置的是主内存中的isRunning,更新了主内存的isRunning,线程工作内存中的isRunning没有更新,当然一直死循环了,因为对于线程来说,它的isRunning依然是true。

解决这个问题很简单,给isRunning关键字加上volatile。加上了volatile的意思是,每次读取isRunning的值的时候,都先从主内存中把isRunning同步到线程的工作内存中,再当前时刻最新的isRunning。看一下给isRunning加了volatile关键字的运行效果:

private volatile boolean isRunning = true;

进入run了
已赋值为false
线程被停止了

看到这下线程停止了,因为从主内存中读取了最新的isRunning值,线程工作内存中的isRunning变成了false,自然while循环就结束了。

volatile的作用就是这样,被volatile修饰的变量,保证了每次读取到的都是最新的那个值。线程安全围绕的是可见性原子性这两个特性展开的,volatile解决的是变量在多个线程之间的可见性,但是无法保证原子性

Volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存重新读取该成员的值,而且,当成员变量值发生变化时,强迫将变化的值重新写入共享内存,这样两个不同的线程在访问同一个共享变量的值时,始终看到的是同一个值

多提一句,synchronized除了保障了原子性外,其实也保障了可见性。因为synchronized无论是同步的方法还是同步的代码块,都会先把主内存的数据拷贝到工作内存中,同步代码块结束,会把工作内存中的数据更新到主内存中,这样主内存中的数据一定是最新的。

 

原子类也无法保证线程安全

原子操作表示一段操作是不可分割的,没有其他线程能够中断或检查正在原子操作中的变量。一个原子类就是一个原子操作可用的类,它可以在没有锁的情况下保证线程安全。

但是这种线程安全不是绝对的,在有逻辑的情况下输出结果也具有随机性,比如

import java.util.concurrent.atomic.AtomicInteger;

public class ThreadDomain29 {
    public static AtomicInteger aiRef = new AtomicInteger();

    public void addNum() {
        System.out.println(Thread.currentThread().getName() + "加了100之后的结果:" + aiRef.addAndGet(100));
        aiRef.getAndAdd(1);
    }
}
public class MyThread29 extends Thread {
    private ThreadDomain29 td;

    public MyThread29(ThreadDomain29 td) {
        this.td = td;
    }

    public void run() {
        td.addNum();
    }

    public static void main(String[] args) {
        try {
            ThreadDomain29 td = new ThreadDomain29();
            MyThread29[] mt = new MyThread29[5];
            for (int i = 0; i < mt.length; i++) {
                mt[i] = new MyThread29(td);
            }
            for (int i = 0; i < mt.length; i++) {
                mt[i].start();
            }
            Thread.sleep(1000);
            System.out.println(ThreadDomain29.aiRef.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

看一下运行结果:

Thread-1加了100之后的结果:100
Thread-2加了100之后的结果:201
Thread-0加了100之后的结果:302
Thread-4加了100之后的结果:403
Thread-3加了100之后的结果:504
505

显然,结果是正确的,但不是我们想要的,因为我们肯定希望按顺序输出加了之后的结果,现在却是200、500、400、300、100这么输出。导致这个问题产生的原因是aiRef.addAndGet(100)和aaiRef.addAndGet(1)这两个操作是可分割导致的。

解决方案,就是给addNum方法加上synchronized即可。

 

posted @ 2022-11-22 17:20  weslie  阅读(46)  评论(0)    收藏  举报