线程的基本概念
线程表示一条单独的执行流,它有自己的程序执行计数器,有自己的栈。
在Java中创建线程有两种方式:一种是继承Thread,另外一种是实现Runnable接口
public class HelloThread extends Thread{
@Override
public void run() {
System.out.println("Thread name: " + Thread.currentThread().getName());
System.out.println("Hello Thread");
}
}
public class HelloRunnable implements Runnable{
@Override
public void run() {
System.out.println("Thread name:" + Thread.currentThread().getName());
System.out.println("Hello Runnable");
}
}
public class Test {
public static void main(String[] args) {
HelloThread helloThread = new HelloThread();
helloThread.start();
Thread helloRunnable = new Thread(new HelloRunnable());
helloRunnable.start();
}
}
通过继承Thread来实现线程虽然比较简单,但Java中只支持单继承,每个类最多只有一个父类。这时,可以通过实现java.lang.Runnable接口来实现线程。
线程的基本属性和方法
线程有一些基本属性和方法,包括id、name、优先级、状态、是否daemo线程、sleep方法、yield方法、join方法等
id 和 name
id是一个递增的整数,每创建一个线程就加一。
name的默认值是Thread-后跟一个编号。
name可以在Thread的构造方法中进行指定,也可以通过setName方法进行设置
// 获取当前线程name
Thread.currentThread().getName()
// 获取当前线程id
Thread.currentThread().getId()
优先级
线程有一个优先级的概念,在Java中,优先级从1到10,默认为5
public final void setPriority(int newPriority);
public final int getPriority();
这个优先级会被映射到操作系统中线程的优先级,不过因为操作系统各不相同,不一定都是10个优先级,Java中不同的优先级可能会被映射到操作系统中相同的优先级。
状态
Thread有一个方法用于获取线程的状态:
public State getState();
返回值类型为Thread.State,它是一个枚举类型
public enum State {
// 没有调用start的线程
NEW,
// 调用start后线程在执行run方法且没有阻塞时。不过,RUNNABLE不代表CPU一定在执行该线程的代码,可能正在执行也可能在等待操作系统分配时间片
RUNNABLE,
// 表示线程被阻塞了
BLOCKED, WAITING, TIMED_WAITING,
// 线程运行结束后
TERMINATED;
}
daemon线程
public final void setDaemon(boolean on);
public final boolean isDaemon();
最开始提到,启动线程会启动一条单独的执行流,整个程序只有在所有线程都结束的时候才退出,但daemon线程是例外,当整个程序中剩下的都是daemon线程的时候,程序就会退出。
daemon线程有什么用呢?它一般是其他线程的辅助线程,在它辅助的主线程退出的时候,它就没有存在的意义了。在我们运行一个即使最简单的程序,实际上Java也会创建多个线程,除了main线程,至少还有一个负责垃圾回收的线程,这个线程就是daemon线程,在main程序结束的时候,垃圾回收程序也会退出
sleep
public static native void slepp(long millis) throws InterruptedException;
调用该方法会让当前线程睡眠指定的时间,单位是毫秒。
睡觉期间,该线程会让出CPU,但睡眠的时间不一定的确切的给定毫秒数,会有一定偏差。
睡眠期间,线程可以被中断,如果被中断,sleep会抛出InterruptedException
yield
public static native void yield();
调用该方法,告诉操作系统的调度器:我现在不着急用CPU,你可以先让其他线程运行
join
在最开始的使用示例中,可能HelloThread没有执行完,但是main线程执行完了,Thread有一个join方法,可以让调用join的线程等待该线程结束
public final void join() throws InterruptedException
在等待线程结束的过程中,这个等待可能被中断,如果被中断,会抛出InterruptedException
join方法还有一个变体,可以限定等待的最长时间,单位为毫秒,如果为0,表示无限等待
public final synchronized void join(long millis) throws InterruptedException
内存共享
每个线程表示一条单独的执行流,有自己的程序计数器、自己的栈。
但线程之间可以共享内存,它们可以访问和操作相同的对象
public class ShareMemoryDemo {
private static int shared = 0;
private static void incrShared() {
shared++;
}
static class ChildThread extends Thread {
List<String> list;
public ChildThread(List<String> list) {
this.list = list;
}
@Override
public void run() {
incrShared();
list.add(Thread.currentThread().getName());
}
}
public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<String>();
ChildThread t1 = new ChildThread(list);
ChildThread t2 = new ChildThread(list);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(shared);
System.out.println(list);
}
}
- 示例代码中有三条执行流,一条执行main方法,另外两条执行ChildThread的run方法
- 不同执行流可以访问和操作相同的变量
- 不同执行流可以执行相同的程序代码
- 当多条执行流执行相同的程序代码时,每条执行流都有单独的栈,方法中的参数和局部变量都有自己的一份
竞态条件
所谓竞态条件(race condition)是指,当多个线程访问和操作同一个对象时,最终执行结果与执行时序有关
public class CounterThread extends Thread {
private static int counter = 0;
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
counter++;
}
}
public static void main(String[] args) throws InterruptedException {
int num = 1000;
Thread[] threads = new Thread[num];
for (int i = 0; i < threads.length; i++) {
threads[i] = new CounterThread();
threads[i].start();
}
for (int i = 0; i < threads.length; i++) {
threads[i].join();
}
System.out.println(counter);
}
}
期望结果是1000万,但实际执行会发现每次输出的结果都不一样,一般都不会是1000万。
因为counter++这个操作不是原子操作
, 它分为三个步骤
1. 取counter的当前值
2. 在当前值基础上加1
3. 将新值重新赋值给counter
两个线程可能同时执行第一步,取到了相同的counter值,比如都取到了100,第一个线程执行完之后counter变为101,而第二个线程执行完后还是101。
解决办法:
1. 使用synchronized关键字
2. 使用显示锁
3. 使用原子变量
内存可见性
多个线程可以共享访问和操作相同的变量,但一个线程对一个共享变量的修改,另一个线程不一定马上就能看到,甚至永远都看不到
public class VisibilityDemo {
private static boolean shutdown = false;
static class HelloThread extends Thread {
@Override
public void run() {
while(!shutdown) {
// do something
}
System.out.println("exit hello");
}
}
public static void main(String[] args) throws InterruptedException {
new HelloThread().start();
Thread.sleep(1000);
shutdown = true;
System.out.println("exit main");
}
}
期望的结果是两个线程都退出,但实际执行时,很可能会发现HelloThread永远都不会退出。这就是内存可见性问题。
在计算机系统中,除了内存,数据还会被缓存在CPU的寄存器以及各级缓存中,当访问一个变量时,可能直接从寄存器或CPU缓存中获取,而不一定到内存中去取,当修改一个变量时,也可能先写到缓存中,稍后才会同步更新到内存中。所以,一个线程对内存的修改,另一个线程看不到,一个修改没有及时同步到内存,二是另一个线程根本就没有从内存读。
解决办法:
1. 使用volatile关键字
2. 使用synchronized关键字或显示锁同步