本篇为 Java编程的逻辑 的并发内容的学习笔记
什么是线程?
线程是表示一条单独执行的程序执行流,它有自己的程序计数器和栈。
创建线程
Java 中的 Thread
对象 实现了 Runnable
接口,因此创建线程的方法有两种。
public class Thread implements Runnable {}
1.继承Thread
通过继承 Thread
并重写其中的 run
方法来实现一个线程:
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("实现了一个线程");
}
}
定义了这么一个类并不代表代码就会立即执行,线程是需要启动的,启动就需要一个 Thread
对象并调用 start
方法:
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
start
方法表示启动该线程,使其成为一个单独的程序执行流 ,操作系统会分配相关资源给它,让它参与 cpu
的抢夺,抢到 cpu
后执行的方法就是 run
方法
但是要怎么确认代码是在哪个线程中执行的呢?
Thread
有一个静态方法可以返回当前执行代码所在的线程对象:
public static native Thread currentThread();
每个线程都有一个 id
和 name
:
public long getId()
public final String getName()
通过上述方法我们可以判断代码是在哪个线程里面执行了,修改 MyThread
里面的 run
方法:
@Override
public void run() {
System.out.println("thread name: " + Thread.currentThread().getName());
System.out.println("实现了一个线程");
}
为了更为直观,我们在main
方法里面也增加类似的获取线程名字的打印,那么输出结果就是:
thread name: main
thread name: Thread-0
实现了一个线程
2.实现Runnable接口
上面的方法实现线程的方法虽然简单,但是 Java 是单继承的,这样子并不利于扩展。所以,推荐都是通过 Runnable
接口来实现线程。
public interface Runnable {
public abstract void run();
}
具体的实现调用方法如下:
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("通过Runnable实现了一个线程");
}
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start();
}
}
实现了 Runnable
接口后还需要一个线程对象来装载它并启动线程,执行 Runnable
接口里面的 run
方法。
线程里面的基本属性和方法
线程里面有一些基本属性和方法,这里简单介绍一下。
1.id和name
这个前面提过了,每个线程都有id
和 name
。
id
是一个整数,这个是递增的整数,只在线程创建时进行加一赋值,不提供额外的修改方法。
name
的默认值是 Thead- 后面跟一个编号,可以通过 setName
进行修改,可以给线程取名来方便调试,但是个人更加推荐使用 id
来进行调试,因为它具备唯一性。
2.线程的优先级
线程有一个优先级的概念,Java 中的线程优先级是 1 到 10 ,默认为 5,优先级相关方法为:
public final void setPriority(int newPriority)
thread.getPriority()
一般来说,优先级越高越容易抢到 CPU
,但是这个并非强制性的,只是相对容易而已,所以实际开发中不要太过依赖线程的优先级。
3.状态
线程里面有一个状态的概念,也可以简单理解为线程生命周期的一部分,通过下面方法获取:
public State getState()
返回对象 State
是一个枚举对象,它有如下值:
public enum State {
/**
* 线程创建了,但是没有调用start()
*/
NEW,
/**
* 线程抢夺CPU中,抢到就执行run()
*/
RUNNABLE,
/**
* 线程阻塞了
*/
BLOCKED,
/**
* 线程等待唤醒的阻塞
*/
WAITING,
/**
* 线程在等待某个时间点给唤醒
*/
TIMED_WAITING,
/**
* 线程运行结束了
*/
TERMINATED;
}
Thread
结束运行了,并不代表它已经死掉了,只能说它将要给回收掉,判断 Thread
是否活着要看这个方法:
public final native boolean isAlive();
只要 Thread
启动后没有给回收,返回值都是 true 。
4.是否守护线程
Java中线程分为两种类型:用户线程和守护线程,默认启动的线程都是用户进程,通过 Thread.setDaemon(true)
将线程设置为守护线程。
守护线程**有什么用呢?它就是个辅助线程,所以当全部的用户线程都给回收后,它就没有存在的意义了。
最典型的例子就是,Java 里面的垃圾回收器就执行在守护线程里面,当 main 线程执行结束后,垃圾回收器也会一起退出了。
5.sleep 方法
Thread
有个静态的 sleep
方法可以让线程让出 CPU
并睡上指定的时间,而且在线程睡眠的情景可以给中断,如果给中断了就会抛出中断异常了。
public static native void sleep(long millis) throws InterruptedException;
6.yield 方法
这个也是一个静态方法,调用该方法是告诉系统: 我现在不急着要抢 CPU
,你可以优先分配给其他线程。
不过,这个并没有强制作用,系统可能会忽略这个声明。
public static native void yield();
7.join 方法
public final void join() throws InterruptedException
如果 A 线程里面启动了 B 线程,B 线程调用了 join
方法的话,那么 A 线程必须等到 B 线程结束之后,才可以结束:
public static void main(String[] args) throws InterruptedException {
MyThread thread = new MyThread();
thread.join();
thread.start();
while (Thread.currentThread().isAlive()) {
System.out.println("Test : " + thread.isAlive());
if (!thread.isAlive()) {
System.out.println("finally : " + thread.isAlive());
break;
}
}
}
这么一来,只有 MyThread
结束了,main 线程才可以结束了。
thread name: Thread-0
实现了一个线程
Test : true
finally : false
join
方法还有一个重载体,可以指定等待时间的时长,默认为0,也就是无限等待了。
public final synchronized void join(long millis) throws InterruptedException
8.过时方法
Thread
类里面还有一些过时的方法,这些方法都有各种隐患,我们不应该去调用它们了,如下:
public final void stop()
public final void suspend()
public final void resume()
线程的成本
我们为什么需要开启其他的线程呢?不少人都知道这样子可以充分利用 CPU
的计算能力和硬件资源,进一步提高效率。
但是,我们也必须要知道,线程是有成本,尤其是大量线程运行时,线程之间调度和切换,有可能让系统卡死。那么,我们程序的最大线程数应该是多少才合适?
在 Java 的并发里面,最常见的两个模型就是 CPU 密集型
和 IO 密集型
,对于这两个模式,最大线程数是不一致的。
CPU 密集型
顾名思义就是需要非常多的 CPU 计算资源,对于这种类型的应用,完全是靠 CPU 的核数来工作,所以为了让它的优势完全发挥出来,避免过多的线程上下文切换,比较理想方案是:
线程数 = CPU核数 + 1
IO 密集型
是指应用里面基本都是涉及到网络、磁盘等 IO 的任务, 一旦发生 IO,线程就会处于等待状态,当 IO结束,数据准备好后,线程才会继续执行。
针对这种并发模型,网上推荐如果是 web 应用的话,理想线程数为:
线程数 = CPU核心数 / (1- 阻塞系数 )
这个阻塞系数一般为0.8~0.9之间,也可以取0.8或者0.9,需要根据实际应用来判断。
但是如果是用在 Android 应用里面的话,网上资料大多推荐为:
线程数 = CPU核数 * 2 + 1