Java--线程

线程的基本概念

主要介绍一些线程的基本概念,包括创建线程、线程的基本属性和方法、共享内存及问题、线程的优点及成本。

创建线程

线程表示一条单独的执行流,它有自己的程序执行计数器,有自己的栈。Java中创建线程有两种方式:一种是继承Thread;另外一种是实现Runnable接口。

1 继承Thread

Java中java.lang.Thread这个类表示线程,一个类可以继承Thread类并重写其run()方法来实现一个线程,如:

public class MyThread extends Thread {
    @Override
    public void run() {
        //逻辑处理
    }
}

MyThread类继承了Thread,并重写了run()方法。run()方法类似于单线程程序中的main()方法,线程从run()方法第一条语句开始执行直到结束。
线程是需要被启动的,先创建一个MyThread对象,然后调用Thread的start方法。

 MyThread thread = new MyThread();
 thread.start();

start方法表示启动该线程,使其成为一条单独的执行流,操作系统会分配线程相关的资源,每个线程会有单独的程序执行计数器和栈,操作系统会把每个线程当作一个独立的个体来进行调度,分配时间片让他执行,执行的起点就是run()方法。
怎么确认代码是在哪个线程中执行呢,Thread有一个静态的方法currentThread,返回当前执行的线程对象,每一个Thread都有一个id和name,这样我们可以判断代码是在哪个线程中执行的。
调用了start后,就有了两条执行流,一条执行run方法,一条执行main方法,两条执行流并发执行,操作系统负责调度,在单CPU的机器上,同一时刻只能有一个线程在执行,在多CPU的机器上,同一时刻可以有多个线程同时执行,但操作系统给我们屏蔽了这种差异,给我们的感觉就是多个线程并发执行,但哪条语句先执行哪条后执行是不一定的。当所有线程都执行完毕的时候,程序退出。

2 实现Runnable接口

通过继承Thread类来创建线程虽然比较简单,但Java中只支持单继承,每个类最多只有一个父类。如果该类已经有一个父类,则不能再继承Thread类了。这时可以通过实现java.lang.Runnable接口来实现线程。
无论通过继承Thread类还是实现Runnable接口来创建线程,启动线程都是调用start方法。

线程的基本属性和方法

1.id和name
每一个线程都有一个id和name。id是一个递增的整数,每创建一个线程就加1,name的默认值是Thread-后跟
一个编号,name可以在Thread的构造方法中进行指定,也可以通过setName()方法进行设置,方便调试。

2.优先级
Java中线程有一个优先级的概念,优先级从1-10,默认是5。

public final void setPriority(int newPriority) {
    throw new RuntimeException("Stub!");
}

public final int getPriority() {
    throw new RuntimeException("Stub!");
}

这个优先级会被映射到操作系统中线程的优先级,不过因为操作系统不同,不一定都是十个优先级。Java中不同的优先级可能会被映射到操作系统中相同的优先级。另外,优先级对操作系统而言是一种建议和提示,而非强制,简单来说,在编程中不要过于依赖线程的优先级。

3.状态
线程有一个状态的概念,Thread有一个方法获取线程的状态。

public Thread.State getState() {
    throw new RuntimeException("Stub!");
}

返回的类型是Thread.State,它是一个枚举类型,有如下值:

public static enum State {
        BLOCKED,
        NEW,
        RUNNABLE,
        TERMINATED,
        TIMED_WAITING,
        WAITING;

        private State() {
        }
    }

状态解释:
① NEW:没有调用start的线程状态为NEW
② TERMINATED: 线程运行结束
③ RUNNABLE: 调用start后,线程在执行run方法并且没有阻塞时状态为RUNNABLE,不过RUNNABLE不代表CPU一定在执行该线程的代码,可能正在执行,也可能在等待操作系统分配时间片,只是它没有在等待其他条件
④ BLOCKED TIMED_WAITING WAITING: 都表示线程被阻塞了,在等待一些条件

Thread还有一个方法,返回线程是否活着。

public final boolean isAlive() {
    throw new RuntimeException("Stub!");
}

线程被启动后,run方法运行结束前,返回值都是true。

4.是否daemon线程
Thread有一个是否daemon线程的属性,相关方法:

public final void setDaemon(boolean on) {
    throw new RuntimeException("Stub!");
}
public final boolean isDaemon() {
    throw new RuntimeException("Stub!");
}

之前提到,启动线程会启动一条单独的执行流,整个程序只有在所有线程都结束的时候才会退出,但daemon线程是例外,当整个程序中剩下的都是daemon线程的时候,程序就会退出。
daemon线程一般是其他线程的辅助线程,在它辅助的主线程退出的时候,它就没有存在的意义了。在我们运行一个最简单的程序的时候,实际上Java也会创建多个线程,除了main线程外,至少还有一个负责垃圾回收的线程,这个线程就是daemon线程,在main线程结束的时候,垃圾回收线程也会退出。

5.sleep方法
Thread有一个静态的sleep方法,调用该方法会让当前的线程睡眠指定的时间,单位是毫秒:

public static void sleep(long millis) throws InterruptedException {
    throw new RuntimeException("Stub!");
}

睡眠期间,该线程会让出CPU,但睡眠时间不一定是确切的给定的毫秒数,可能有一定的偏差,偏差与系统定时器和操作系统调度器的准确度和精度有关。睡眠期间,线程可以被中断,如果被中断,sleep会抛出InterruptedException。

6.yield方法
Thread还有一个让出CPU的方法,也是一个静态方法:

public static native void yield();

调用该方法是告诉操作系统调度器:现在不着急使用CPU,可先让其他线程运行。不过这对调度器也仅仅是建议,调度器如何处理是不一定的,它可能完全忽略该调用。

7.join方法
子线程没执行完,main线程可能就执行完了,Thread有一个join方法,可以让main线程等待调用join的线程结束(即等待子线程结束)。也可以在子线程中再开启一个子线程,例如在子线程1中,启动子线程2,子线程2调用join方法,则子线程1会等待子线程2结束。join的方法:

public final void join() throws InterruptedException {
    throw new RuntimeException("Stub!");
}

在等待线程结束的过程中,这个等待可能被中断,如果被中断,会抛出InterruptedException。
join方法有一个变体,可以限定等待的最长时间,单位为毫秒,如果为0,表示无期限等待:

public final void join(long millis) throws InterruptedException {
    throw new RuntimeException("Stub!");
}

共享内存及可能存在的问题

每个线程表示一条单独的执行流,有自己的程序计数器有自己的栈,但线程之间可以共享内存,它们可以访问和操作相同的对象。不同的执行流可以执行相同的程序代码,在分析代码执行过程时,理解代码在被哪个线程执行是非常重要的。多条执行流执行相同的代码时,每条执行流都有单独的栈,方法中的参数和局部变量都有自己的一份。当多条执行流操作相同的变量时,可能会出现一些意料之外的结果,包括竞态条件和内存可见性问题。

1.竞态条件
所谓竞态条件是指,当多个线程访问和操作同一个对象时,最终执行结果和执行时序有关,可能正确也可能不正确。示例:

public class MyThread extends Thread {
    private static int counter = 0;

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            counter++;
        }
    }

    public static void main(String[] args) {
        Thread[] threads = new Thread[1000];
        for (int i = 0; i < 1000; i++) {
            threads[i] = new MyThread();
            threads[i].start();
        }
        for (int i = 0; i < 1000; i++) {
            try {
                threads[i].join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(counter);
    }
}

在main方法中创建了1000个线程,每个线程对变量counter循环加1000次,main线程等所有线程执行结束后返回counter值。期望结果100W,但每次运行发现结果都不是100W, 为什么会这样,因为counter++这个操作不是原子操作,分为三步:
① 取counter当前值
② 当前值基础上加1
③ 将新值重新赋值给counter
比如有两个线程同时取到相同的counter值100,第一个线程执行完递加后,值为101,第二个同样也是101,就造成了结果不准确。

2.内存可见性
多个线程可以共享访问和操作相同的变量,但一个线程对共享变量的修改,另一个线程不一定马上就能看到,甚至永远都看不到。举例如下:

public class VisibilityDemo {
    private static boolean isShut = false;
    static class ChildThread extends Thread{
        @Override
        public void run() {
            while (!isShut){

            }
            System.out.println("ChildThread End");
        }
    }
    public static void main(String[] args) throws InterruptedException {
        new ChildThread().start();
        Thread.sleep(1000);
        isShut = true;
        System.out.println("mainThread End");
    }
}

这段代码中静态变量isShut默认值为false,main方法中启动子线程后,会进入一个死循环,主线程睡眠1S后,将isShut变量赋值为true,此时跳出死循环,子线程结束,主线程也跟着结束。但实际执行发现子线程可能永远都不会退出,也就是说在子线程看来isShut永远为false,即使主线程中已将其更改为true。
这种情况就是内存可见性问题,在计算机系统中,除了内存,数据还会被缓存在CPU的寄存器以及各级缓存中,当访问一个变量时,可能直接从寄存器或CPU缓存中获取,而不一定到内存中去取,当修改一个变量时,也有可能是先缓存到内存中,稍后才会同步更新到内存中。在单线程中,这一般不是问题,但在多线程的程序中,尤其是多CPU的情况下,这就是严重的问题。一个线程对内存的修改,另一个线程看不到,一是修改没有及时的同步到内存,二是另一线程可能根本就没从内存读取。

线程的优点及成本

优点:
① 充分利用多CPU的计算能力,单线程只能利用一个CPU,使用多线程可以使用多CPU的计算能力。
② 充分利用硬件资源,CPU和硬盘、网络是可以同时工作的,一个线程在等待网络IO的同时,另一个线程完全可以利用CPU,对于多个独立的网络请求,完全可以使用多个线程同时请求。
③ 在用户界面应用程序中,保持程序的响应性,界面和后台任务通常是不同的线程,如果所有事情让一个线程来执行,当执行一个很慢的任务时,整个界面将停止响应,也无法取消该任务。
④ 简化建模及IO处理,比如,在服务器应用程序中,对每个用户请求使用一个单独的线程进行处理,相比使用一个线程,处理来自各种用户的各种需求,以及各种网络和文件IO事件,建模和编写程序要容易得多。

关于线程我们需要知道,它是有成本的。创建线程需要消耗操作系统的资源,操作系统会为每个线程创建必要的数据结构、栈、程序计数器等,创建也需要一定的时间。
此外线程调度和切换也是有成本的,当有大量可运行线程的时候,操作系统忙于调度,为一个线程分配一段时间执行完后,再让另一个线程执行,一个线程被切换出去后,操作系统需要保存它的当前上下文状态到内存,上下文状态包括当前CPU寄存器的值、程序计数器的值等,而一个线程被切换回来后,操作系统需要恢复它原来的上下文状态,整个过程称为上下文切换,这个切换不仅耗时,而且使CPU中的很多缓存失效。当然这是相对而言的,如果线程中实际执行的事情比较多,这些成本是可以接受的。另外,如果执行的任务都是CPU密集型的,即主要消耗的都是CPU,那创建超过CPU数量的线程就是没有必要的,并不会加快程序的执行。

拓展
CPU:中央处理器 central processing unit 计算机系统的运算和控制核心,是信息处理、程序运行的最终执行单位。电子计算机的主要设备之一,核心配件。其功能主要是解释计算机指令以及处理计算机软件中的数据。CPU是计算机中负责读取指令,对指令译码并执行指令的核心部件。中央处理器主要包括两个部分,即控制器、运算器,其中还包括高速缓冲存储器及实现它们之间联系的数据、控制的总线。
我们接触的电脑基本上都只有一个CPU,CPU的个数很容易得到。为了给电脑更高的性能,一个CPU中集成了了多个内核,这样电脑的性能就能成倍的提升。随着科技发展,每个内核的性能也十分的强大,于是一个内核又被分成两个线程。我们需要注意的是,一个CPU分成多个内核,这是物理的分隔,拆开CPU是可以看到的,但是一个内核被分成两个线程是一种超线程技术,操作系统会认为一个线程也是一个内核,有点欺骗操作系统的感觉。若说CPU有四核,这时我们要提高警惕,是真四核还是四线程(假四核)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值