基本概念
线程可以看作是程序中的执行路径,多线程指的就是程序中有多个执行路径同时执行。
线程是一个程序内部的顺序控制流。
线程和进程的区别
每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大开销。
线程可以看成是轻量级的进程,同一类线程共享代码和数据空间,每个线程都有独立的运行栈和程序计数器(PC),线程切换的开销较小。
进程是一个静态概念,在程序中实际执行的,都是线程。
多进程:操作系统中同时运行多个任务(程序)。
多线程:在同一应用程序中有多个顺序流同时执行。
在同一个时间点,cpu只会执行一个线程,多个线程轮流得到cpu的时间片,只不过cpu的运行速度过于迅速,看起来像同时执行多个线程一样。但多核的cpu能够实实在在的同时执行多个线程。
VM启动时会有一个主方法(main方法)所定义的线程,该线程叫做主线程。
线程的创建和启动
每个线程都是通过某个特定Thread对象所定义的run()方法来完成其操作,方法run()称为线程体。
通过调用Thread类的start()方法来启动线程。
方式一
定义一个类,让其继承自Thread类,重写Thread类的run()方法,然后创建一个该类(线程类)对象(线程对象)。
实例
public class Thread1 extens Thread {
//重写run()方法
public void run() {
for(int i = 0;i < 100;i++) {
System.out.println("Thread1:" + i);
}
}
}
//测试类
public class ThreadTest {
public static void main(String[] args) {
//创建线程对象
Thread1 t1 = new Thread1();
//启动线程
t1.start();
for(int i = 0;i < 100;i++) {
System.out.println("MainThread:" + i);
}
}
}
输出结果为
从打印结果可以分析,main方法中执行t1.start()方法之后,主线程中就分离出了一个执行路径,t1线程和主线程同时执行(实际上是两个线程轮流使用cpu,但获取cpu时间片时间的长短不确定)。
方式二
定义一个类,让其实现Runnable接口。然后创建一个该类(线程类)的对象,然后创建一个Thread类对象,以上面的线程对象为参数。
Runnable接口中只有一个方法run(),该方法用于定义线程运行体。
使用Runnable接口可以为多个线程提供共享的数据。
在实现Runnable接口的类的run()方法定义中可以使用Thread类的静态方法:
public static Thread currentThread()获取当前线程的引用。
实际上,Thread类也实现了Runnable接口。
实例
public class Runnable1 implements Runnable {
//实现run()方法
public void run() {
for(int i = 0;i < 100;i++) {
System.out.println("Runnable1:" + i);
}
}
}
//测试类
public class RunnableTest {
public static void main(String[] args) {
Runnable1 r1 = new Runnable1();
Thread t1 = new Thread(r1);
t1.start();
for(int i = 0;i < 100;i++) {
System.out.println("MainThread:" + i);
}
}
}
打印结果
线程控制的基本方法
sleep方法
可以调用Thread类的静态方法:
public static void sleep(long millis) throws InterruptedException;
使得当前线程休眠(暂时停止执行millis毫秒)。
实例
public class Thread2 extends Thread {
public void run() {
while(true) {
//打印当前系统日期
System.out.println("======" + new Date() + "======");
try {
//线程休眠1秒钟
sleep(1000);
} catch (InterruptedException e) {
//当前线程的休眠被打断,while循环结束
return;
}
}
}
}
//测试类
public class SleepTest {
public static void main(String[] args) {
Thread2 t2 = new Thread2();
t2.start();
try {
//主线程休眠10秒钟
Thread.sleep();
} catch (InterruptedException e) {
e.printStackTrace();
}
//打断t2线程的休眠
t2.interrupt();
}
}
打印结果
上面代码的执行过程为,主线程执行到t2.start()时,t2线程开始执行,每个1秒打印当前系统时间,主线程开始休眠10秒钟,10秒过后,主线程执行t2.interrupt()方法,线程t2中的循环结束,停止打印。
join方法
该方法用于线程合并。
实例
public class Thread3 extends Thread {
Thread3(String name) {
super(name);
}
public void run() {
for(int i = 0;i < 10;i++) {
//打印线程名称
System.out.println("I am " + getName());
}
System.out.println("======" + new Date() + "======");
try {
sleep(5000);
} catch (InterruptedException e) {
return;
}
}
}
//测试类
public class JoinTest {
public static void main(String[] args) {
Thread3 t3 = new Thread3();
t3.start();
try {
//将t3线程与主线程合并
t3.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("======" + new Date() + "======"):
for(int i = 0;i < 10;i++) {
System.out.println("I am MainThread");
}
}
}
打印结果
从打印结果可以看到,线程合并之后主线程要等到t3线程代码执行完之后才会执行。
yield方法
让出cpu,让其他线程有执行的机会。
实例
public class Thread4 extends Thread {
Thread4(String name) {
super(name);
}
public void run() {
for(int i = 1;i <= 100;i++) {
System.out.println(getName() + ": " + i);
//当前线程打印到10、20、30...后让与其他线程执行
if(i % 10 == 0) {
yield();
}
}
}
}
//测试类
public class YieldTest {
public static void main(String[] args) {
Thread4 t1 = new Thread4();
Thread4 t2 = new Thread4();
t1.start();
t2.start();
}
}
打印结果
线程状态转换
线程优先级
Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程。线程调度器按照线程的优先级决定先调度哪一个线程来执行。
线程优先级用数字表示,从低到高用数字1~10来表示,默认优先级为5.
Thread.MIN_PRIORITY=1;
Thread.NORM_PRIORITY=5;
Thread.MAX_PRIORITY=10;
获取当前线程优先级方法int getPriority().
设置线程对象优先级void setPriority(int newPriority);
如t1.setPriority(Thread.NORM_PRIORITY + 3)就是将t1线程的优先级设为8.
实例
public class Thread5 extends Thread {
public void run() {
for(int i = 0;i < 1000;i++) {
System.out.println("T5======" + i):
}
}
}
public class Thread6 extends Thread {
public void run() {
for(int i = 0;i < 1000;i++) {
System.out.println("T6======" + i):
}
}
}
//测试类
public class ThreadTest {
public static void main(String[] args) {
Thread5 t5 = new Thread5();
Thread6 t6 = new Thread6();
t5.start();
t6.start();
}
}
打印结果
可以看到,两个线程获取cpu时间片的使用权几乎相同。
然后稍微改一下测试类:
Thread5 t5 = new Thread5();
Thread6 t6 = new Thread6();
//为线程t5设置更大的优先级
t5.setPriority(Thread.NORM_PRIORITY + 3);
t5.start();
t6.start();
再次执行,打印结果为
可以看到,前面执行的线程几乎全是t5,t6几乎要等到t5执行完之后才能执行。
线程同步
多个线程访问同一资源可能会出现多种不可预知的结果。
实例
public class Timer {
private static int num = 0;
public void add(String name) {
num++;
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + "你是第" + num + "个使用timer的线程");
}
}
//测试类
public class SyncTest implements Runnable {
Timer timer = new Timer();
public void run() {
timer.add(Thread.currentThread().getName());
}
public static void main(String[] args) {
SyncTest syncTest = new SyncTest();
Thread t1 = new Thread(syncTest);
Thread t2 = new Thread(syncTest);
t1.setName("t1");
t2.setName("t2");
t1.start();
t2.start();
}
}
打印结果
与想象中的"t1你是第1个使用timer的线程,t2你是第2个使用timer的线程"不一致。
这是因为t1线程执行调用add方法之后,num变成1,接着t1线程休眠1毫秒,t2线程执行接着调用add方法,num变成2,接着t1和t2线程相继醒过来,执行打印语句。其实不用写sleep()方法也可能会发生这种问题,只不过这样是将问题放大。
解决办法
加synchronized关键字
重新定义Timer类中的add方法
public void add(String name) {
//锁定当前对象(被synchronized关键字包裹的代码执行过程中,不能被其他线程打断)
synchronized(this) {
num++;
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + "你是第" + num + "个使用tImer的线程");
}
}
或者直接在方法定义时在void前面加synchronized关键字,表示该方法为同步方法
public synchronized void add() {}
这样做是在当前方法的执行过程中,锁定当前对象。
再次执行测试方法,打印结果变成
synchronized关键字,又被称为对象互斥锁,保证了共享数据操作的完整性。每个对象都对应于一个可称为"互斥锁"的标记。该标记保证在任意时刻,只能有一个线程访问该对象。
死锁
当两个或两个以上的线程都再等待对方执行完毕才能往下执行时,就会发生死锁。
实例
public class DeadLockTest implements Runnable {
public int flag = 1;
static Object o1 = new Object,o2 = new Object();
public void run() {
System.out.println("flag:" + flag);
if(flag == 1) {
synchronized(o1) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(o2) {
System.out.println("1");
}
}
}
if(flag == 0) {
synchronized(o2) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(o1) {
System.out.println("2");
}
}
}
}
public static void main(String[] args) {
DeadLockTest d1 = new DeadLockTest();
DeadLockTest d2 = new DeadLockTest();
d1.flag = 1;
d2.flag = 0;
Thread t1 = new Thread(d1);
Thread t2 = new Thread(d2);
t1.start();
t2.start();
}
}
输出结果
可以看到,程序在打印完flag=0,flag=1之后一直等待,进入死锁状态。
执行过程为:
线程t1启动,打印flag = 1,走第一个if分支,对o1对象加锁,然后线程进入休眠状态500毫秒;线程t2启动,打印flag = 0,走第二个if分支,对o2对象加锁,然后线程进入休眠状态500毫秒;500毫秒过后,线程t1只要再获得o2的对象锁就可以打印字符串1结束,线程t2只要再获得o1的对象锁就可以打印字符串2结束,于是就发生了死锁。
解决死锁的方法:只锁定一个对象,将锁的粒度加粗(比如锁定整个方法,而不是只锁定方法内的部分代码)。