Java线程基础详解
一、线程创建的几种方式
1、继承Thread类创建线程类
- 定义一个类,并集成Thread类,然后重新run方法
- 通过实例化该类,并且调用该实例的start方法即可启动线程,具体代码如下:
public class TestThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("我在看代码+++++++++++++++"+i);
}
}
public static void main(String[] args) {
Thread thread = new TestThread();
thread.start();
for (int i = 0; i < 1000; i++) {
System.out.println("我在学习多线程+++++++++++++++"+i);
}
}
}
2、 实现Runnable接口,重写run方法,执行线程需要丢入Runnable接口实现类,调用start方法。
- 定义Runnable接口的实现类,并重写该接口的run()方法;
- 创建Runnable实现类的实例,并以此实例作为Thread的target对象,即该Thread对象才是真正的线程对象。
- 避免单继承的局限性,方便同一个对象被多个线程使用
public class TestThread2 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("我在看代码+++++++++++++++"+i);
}
}
public static void main(String[] args) {
//创建Runnable接口实现类对象
TestThread2 thread2 = new TestThread2();
//调用Thread执行线程
new Thread(thread2).start();
for (int i = 0; i < 1000; i++) {
System.out.println("我在学习多线程+++++++++++++++"+i);
}
}
}
3、 通过Callable和Future创建线程
- 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
- 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
- 使用FutureTask对象作为Thread对象的target创建并启动新线程。
- 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值其中,Callable接口(也只有一个方法)定义如下:
public class TestCallable implements Callable<Boolean> {
@Override
public Boolean call() throws Exception {
return true;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
TestCallable testCallable1 = new TestCallable();
TestCallable testCallable2 = new TestCallable();
TestCallable testCallable3 = new TestCallable();
//创建执行任务
ExecutorService executorService = Executors.newFixedThreadPool(3);
//提交执行
Future<Boolean> result1 = executorService.submit(testCallable1);
Future<Boolean> result2 = executorService.submit(testCallable2);
Future<Boolean> result3 = executorService.submit(testCallable3);
//获取结果
boolean reset1 = result1.get();
boolean reset2 = result2.get();
boolean reset3 = result3.get();
//关闭服务
executorService.shutdownNow();
}
}
二、线程生命周期
-
1、线程的生命周期包含5个阶段,包括:新建、就绪、运行、阻塞、销毁。具体如图所示:
-
新建状态: 用new关键字和Thread类或其子类建立一个线程对象后,该线程对象就处于新生状态。处于新生状态的线程有自己的内存空间,通过调用start方法进入就绪状态(runnable)。
注意:不能对已经启动的线程再次调用start()方法,否则会出现Java.lang.IllegalThreadStateException异常。 -
就绪状态:处于就绪状态的线程已经具备了运行条件,但还没有分配到CPU,处于线程就绪队列(尽管是采用队列形式,事实上,把它称为可运行池而不是可运行队列。因为cpu的调度不一定是按照先进先出的顺序来调度的),等待系统为其分配CPU。等待状态并不是执行状态,当系统选定一个等待执行的Thread对象后,它就会从等待执行状态进入执行状态,系统挑选的动作称之为“cpu调度”。一旦获得CPU,线程就进入运行状态并自动调用自己的run方法。
提示:如果希望子线程调用start()方法后立即执行,可以使用Thread.sleep()方式使主线程睡眠一伙儿,转去执行子线程。 -
运行状态:处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
处于就绪状态的线程,如果获得了cpu的调度,就会从就绪状态变为运行状态,执行run()方法中的任务。如果该线程失去了cpu资源,就会又从运行状态变为就绪状态。重新等待系统分配资源。也可以对在运行状态的线程调用yield()方法,它就会让出cpu资源,再次变为就绪状态。注: 当发生如下情况是,线程会从运行状态变为阻塞状态: ①、线程调用sleep方法主动放弃所占用的系统资源 ②、线程调用一个阻塞式IO方法,在该方法返回之前,该线程被阻塞 ③、线程试图获得一个同步监视器,但更改同步监视器正被其他线程所持有 ④、线程在等待某个通知(notify) ⑤、程序调用了线程的suspend方法将线程挂起。不过该方法容易导致死锁,所以程序应该尽量避免使用该方法。 当线程的run()方法执行完,或者被强制性地终止,例如出现异常,或者调用了stop()、desyory()方法等等,就会从运行状态转变为死亡状态。
-
阻塞状态:处于运行状态的线程在某些情况下,如执行了sleep(睡眠)方法,或等待I/O设备等资源,将让出CPU并暂时停止自己的运行,进入阻塞状态。
在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续运行 -
死亡状态:当线程的run()方法执行完,或者被强制性地终止,就认为它死去。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。 如果在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。
三、线程管理
Java提供了一些便捷的方法用于会线程状态的控制。具体如下:
- 1、线程睡眠--sleep()
如果我们需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread的sleep方法。 - 注意
(1)sleep是静态方法,最好不要用Thread的实例对象调用它,因为它睡眠的始终是当前正在运行的线程,而不是调用它的线程对象,它只对正在运行状态的线程对象有效。如下面的例子:
public class Test1 {
public static void main(String[] args) throws InterruptedException {
System.out.println(Thread.currentThread().getName());
MyThread myThread=new MyThread();
myThread.start();
myThread.sleep(1000);//这里sleep的就是main线程,而非myThread线程
Thread.sleep(10);
for(int i=0;i<100;i++){
System.out.println("main"+i);
}
}
}
(2)Java线程调度是Java多线程的核心,只有良好的调度,才能充分发挥系统的性能,提高程序的执行效率。但是不管程序员怎么编写调度,只能最大限度的影响线程执行的次序,而不能做到精准控制。因为使用sleep方法之后,线程是进入阻塞状态的,只有当睡眠的时间结束,才会重新进入到就绪状态,而就绪状态进入到运行状态,是由系统控制的,我们不可能精准的去干涉它,所以如果调用Thread.sleep(1000)使得线程睡眠1秒,可能结果会大于1秒。
- 2、线程礼让--yield()
yield()方法和sleep()方法有点相似,它也是Thread类提供的一个静态的方法,它也可以让当前正在执行的线程暂停,让出cpu资源给其他的线程。但是和sleep()方法不同的是,它不会进入到阻塞状态,而是进入到就绪状态。yield()方法只是让当前线程暂停一下,重新进入就绪的线程池中,让系统的线程调度器重新调度器重新调度一次,完全可能出现这样的情况:当某个线程调用yield()方法之后,线程调度器又将其调度出来重新进入到运行状态执行。
实际上,当某个线程调用了yield()方法暂停之后,优先级与当前线程相同,或者优先级比当前线程更高的就绪状态的线程更有可能获得执行的机会,当然,只是有可能,因为我们不可能精确的干涉cpu调度线程。用法如下:
public class Test1 {
public static void main(String[] args) throws InterruptedException {
new MyThread("低级", 1).start();
new MyThread("中级", 5).start();
new MyThread("高级", 10).start();
}
}
class MyThread extends Thread {
public MyThread(String name, int pro) {
super(name);// 设置线程的名称
this.setPriority(pro);// 设置优先级
}
@Override
public void run() {
for (int i = 0; i < 30; i++) {
System.out.println(this.getName() + "线程第" + i + "次执行!");
if (i % 5 == 0)
Thread.yield();
}
}
}
注:关于sleep()方法和yield()方的区别如下:
①、sleep方法暂停当前线程后,会进入阻塞状态,只有当睡眠时间到了,才会转入就绪状态。而yield方法调用后 ,是直接进入就绪状态,所以有可能刚进入就绪状态,又被调度到运行状态。
②、sleep方法声明抛出了InterruptedException,所以调用sleep方法的时候要捕获该异常,或者显示声明抛出该异常。而yield方法则没有声明抛出任务异常。
③、sleep方法比yield方法有更好的可移植性,通常不要依靠yield方法来控制并发线程的执行。
- 3、线程合并—--join
线程的合并的含义就是将几个并行线程的线程合并为一个单线程执行,应用场景是当一个线程必须等待另一个线程执行完毕才能执行时,Thread类提供了join方法来完成这个功能,注意,它不是静态方法。
从上面的方法的列表可以看到,它有3个重载的方法:
void join()
当前线程等该加入该线程后面,等待该线程终止。
void join(long millis)
当前线程等待该线程终止的时间最长为 millis 毫秒。 如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度
void join(long millis,int nanos)
等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度
- 4、观测线程状态--getState
线程可以处于一下状态之一: - NEW:尚未启动的线程处于此状态。
- RUNNABLE:在JAVA虚拟机中执行的线程处于此状态。
- BLOCKED:被阻塞等待监视器锁定的线程处于此状态。
- WATTING:正在等待另一个线程执行特定动作的线程处于此状态。
- TIMED_WATTING:正在等待另一个线程执行动作达到指定等待时间的线程处于此状态。
- TERMINATED:已退出的线程处于此状态。
具体实现代码如下:
public class TestState {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(()->{
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("//////////////");
});
Thread.State state = thread.getState();
System.out.println(state); //NEW
thread.start();
state = thread.getState();
System.out.println(state);//RUN
while (Thread.State.TERMINATED != state){
Thread.sleep(100);
state = thread.getState();
System.out.println(state);
}
}
}
- 5、线程优先级
每个线程执行时都有一个优先级的属性,优先级高的线程可以获得较多的执行机会,而优先级低的线程则获得较少的执行机会。与线程休眠类似,线程的优先级仍然无法保障线程的执行次序。只不过,优先级高的线程获取CPU资源的概率较大,优先级低的也并非没机会执行。
每个线程默认的优先级都与创建它的父线程具有相同的优先级,在默认情况下,main线程具有普通优先级。
注:Thread类提供了setPriority(int newPriority)和getPriority()方法来设置和返回一个指定线程的优先级,其中setPriority方法的参数是一个整数,范围是1~·0之间,也可以使用Thread类提供的
三个静态常量:
MAX_PRIORITY =10
MIN_PRIORITY =1
NORM_PRIORITY =5
public class TestPriority {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName()+"---->"+ Thread.currentThread().getPriority());
MyPriority myPriority = new MyPriority();
Thread t1 = new Thread(myPriority);
Thread t2 = new Thread(myPriority);
Thread t3 = new Thread(myPriority);
Thread t4 = new Thread(myPriority);
Thread t5 = new Thread(myPriority);
Thread t6 = new Thread(myPriority);
t1.start();
t2.setPriority(2);
t2.start();
t3.setPriority(4);
t3.start();
t4.setPriority(Thread.MAX_PRIORITY);
t4.start();
t5.setPriority(6);
t5.start();
t6.setPriority(8);
t6.start();
}
}
class MyPriority implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"---->"+ Thread.currentThread().getPriority());
}
}
- 6、守护线程(daemon)
- 守护线程使用的情况较少,但并非无用,举例来说,JVM的垃圾回收、内存管理等线程都是守护线程。还有就是在做数据库应用时候,使用的数据库连接池,连接池本身也包含着很多后台线程,监控连接个数、超时时间、状态等等。调用线程对象的方法setDaemon(true),则可以将其设置为守护线程。守护线程的用途为:
- 守护线程通常用于执行一些后台作业,例如在你的应用程序运行时播放背景音乐,在文字编辑器里做自动语法检查、自动保存等功能。
- Java的垃圾回收也是一个守护线程。守护线的好处就是你不需要关心它的结束问题。例如你在你的应用程序运行的时候希望播放背景音乐,如果将这个播放背景音乐的线程设定为非守护线程,那么在用户请求退出的时候,不仅要退出主线程,还要通知播放背景音乐的线程退出;如果设定为守护线程则不需要了。
如下示例:
public class TestDaemon {
public static void main(String[] args) {
God god = new God();
You you = new You();
Thread thread = new Thread(god);
thread.setDaemon(true);//默认为false表示用户线程,正常的线程都是用户线程
thread.start();
new Thread(you).start();//你,用户线程启动......
}
}
class God implements Runnable{
@Override
public void run() {
while (true) {
System.out.println("上帝保护着你!");
}
}
}
class You implements Runnable{
@Override
public void run() {
for (int i = 0; i < 36500; i++) {
System.out.println("你活得很开心");
}
System.out.println("=========GoodBye World=========");
}
}
四、线程同步
- 1、初始并发问题:关于多线程购票问题,具体实现代码如下:
public class TestThread3 implements Runnable{
private int titckNum = 10;
@Override
public void run() {
while (true){
if(titckNum <= 0){
break;
}
try {
//模拟延时
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"拿到了第"+ titckNum-- +"票");
}
}
public static void main(String[] args) {
TestThread3 thread3 = new TestThread3();
new Thread(thread3,"小明").start();
new Thread(thread3,"老师").start();
new Thread(thread3,"黄牛").start();
}
}
总结:以上示例出现了线程不安全的问题,即购买的票最后可能为负数,也可能出现同一张票被多人购买。线程安全:多线程的情况下对同一个变量或者对象进行读写操作,结果是唯一,不可变的称为线程安全,反之则线程不安全。而线程安全的问题出现在并发的情况下,如上述购票即使并发情况下导致线程不安全。并发:同一个对象被多个线程同时操作。那么如何保证线程安全,就出现了线程同步问题,线程同步问题可以简单看成是个等待机制,多个需要同时访问此对象的线程进入这个对象的线程等待池形成队列,等待前面线程使用完毕,下一个线程在使用。