Java多线程基础梳理


目录

  • Java多线程基础梳理
  • 一、什么是线程
  • 二、线程创建的常见的两种方式
  • 1 继承java.lang.Thread类方式
  • 2 继承java.lang.Runnable()接口方式
  • 3 两种方式对比
  • 三、线程的生命周期及线程的优先级
  • 1、生命周期
  • 2、优先级
  • 四、Thread的晦涩方法
  • 五、线程的同步
  • 1、同步监视器(锁)
  • 2、同步代码块
  • 3、同步方法
  • 六、线程的通信
  • 1、wait方法
  • 2、notify和notifyAll方法
  • 3、注意
  • 七、其他
  • 1、jdk5.0后新增的线程创建方式
  • 2、ThreadLocal
  • 3、ConcurrentHashMap


一、什么是线程

要理解线程,我们可以先来讨论下什么是进程,本质上讲进程就是运行起来的程序。以Windows系统举例,Win系统时多任务的操作系统,以进程为资源分配单位,系统会为每个进程分配CPU时间片,此进程可以在这个时间片内使用CPU,而下个时间片可能就会分配给其他的进程了。

而同一个进程中又可以包含很多个并发执行的线程,线程作为调度和执行的最小单位,每个线程拥有独立的虚拟机栈和程序计数器。相对于直接切换进程来看,线程的切换开销更小。

二、线程创建的常见的两种方式

1 继承java.lang.Thread类方式

步骤:创建一个类MyThread继承Thread类,并重写其run方法;run方法就是此线程要执行功能的代码;创建MyThread的对象并调用start方法开启线程。

public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            if (i % 2 == 0){
                System.out.println(getName()+ "输出:" + i);
            }
        }
    }
}
public class MyThreadTest {
	public static void main(String[] args) {
		MyThread myThread = new MyThread();
		myThread.setName("线程1");
		myThread.start();
	}
}
2 继承java.lang.Runnable()接口方式

步骤:(1)创建一个实现了Runnable接口的类,实现其中的run方法;(2)创建实现类的对象,并将此对象作为参数创建Thread类的对象;(3)以Thread类的对象调用start方法开启线程。

public class MyRun implements Runnable{
	int i = 100;
    @Override
    public void run() {
        for (; i >= 0; i--) {
            if (i % 2 == 0){
                System.out.println(Thread.currentThread().getName()+ "输出:" + i);
            }
        }
    }
}
public class MyThreadTest2 {
    public static void main(String[] args) {
        MyRun myRun = new MyRun();
        Thread myThread1 = new Thread(myRun);
        myThread1.setName("线程1");
        myThread1.start();

		Thread myThread2 = new Thread(myRun);
        myThread2.setName("线程2");
        myThread2.start();
    }
}
3 两种方式对比

事实上,我们通过源码可以看到,Thread也是一个实现了Runnable接口的类,所以继承Thread的方式本质上也是间接地实现了Runnable接口。
但是我们思考一下,可能在实际开发中,一个类有其本来的含义,可能要去继承一些比较实际的类,以达到其本来的目的。而我们又知道Java中是不支持多继承的,所以似乎第二种方式更加的实用一些。

实现Runnable接口的方式,没有类单继承的局限性,并且更加适合处理多线程中有数据共享的情况。怎么理解最后这句话呢?我们可以看一下上面的这个代码,两个线程我们只需要创建一个MyRun的对象即可,这样对象的 i 属性可以共用。

三、线程的生命周期及线程的优先级

1、生命周期

在Java中线程生命周期共包含6个状态,在Thread的内部枚举类State中我们可以看到,分别是New,Runnable,Blocked,Waiting,Timed_Waiting,Terminated。其中Blocked、Waiting、Timed_Waiting统称为阻塞状态。

新建的线程为New状态,获得执行权正在执行的线程为Runnable状态,此线程执行完了之后为Terminated状态。
状态之间的转换,在源码中描述的很详细,而且很容易理解。

2、优先级

线程的优先级从 MIN_PRIORITY:1 到 MAX_PRIORITY:10,默认优先级为NORM_PRIORITY:5。可以用getPriority()和setPriority(int)查看和修改线程的优先级。

对优先级的理解,高优先级的线程可以抢占低优先级的线程的时间片,高优先级的线程有更大的概率获取到执行权,但绝不是高优先级的一定先执行。

四、Thread的晦涩方法

1、static yield(),“让步”,API中是这样解释的:“提示线程调度器,表明当前线程愿意放弃当前对处理器的使用。调度程序可以忽略这个提示。Yield是一种启发式的尝试,旨在提高线程之间的相对进程,否则就会过度使用CPU。”
我认为此方法只是提出申请释放CPU资源,至于能否成功释放由JVM决定。不会释放锁,线程依然处于RUNNABLE状态。

2、sleep(int millis),使当前正在执行的线程在指定毫秒数内休眠(暂停执行),线程不会释放锁,但是线程转为Timed_Waiting状态。

3、join()/join(int millis),“插队”,假设目前运行线程A,在线程A里面调用了线程B.join方法,则接下来线程B会抢先在线程A面前执行,等到线程B全部执行完后才继续执行线程A。带参数这个表示最多等待millis时间。

五、线程的同步

当多个线程需要访问同一个资源时,可能存在一些线程安全问题,此时我们需要使用同步机制。

1、同步监视器(锁)

任何类的对象都可以充当锁,但是要求被同步的线程共用同一把锁。

所以在实现Runnable接口的方式(如二、2节)中,可以考虑使用this(此对象),因为两个线程使用同一个Runnable接口的实现类对象。在继承Thread类的实现方式中,可以考虑当前类充当监视器,因为当前类也是Class的对象,而且是唯一的。

2、同步代码块
synchronized (同步监视器){
	// 需要被同步的代码
}

同步代码块中的内容,每次只能由一个线程访问,进入同步代码块前此线程会获得锁,直到此代码块中的代码全部执行完之后才会释放锁。

3、同步方法
public synchronized void methodName(){}

如果操作贡献数据(被同步)的代码完整的在一个方法中,那么可以把其定义为同步方法。虽然看定义,同步方法并没有提及到同步监视器,但其实仍然涉及到同步监视器,只不过不需要显示的声明,非静态同步方法默认是this(此对象),而静态的同步方法中,是当前类本身冲到同步监视器。

六、线程的通信

线程与线程之间不是相互独立的个体,它们彼此之间需要相互通信和协作,比如“生产者消费者问题”。

1、wait方法

调用wait()方法的线程会进入阻塞状态,同时会释放同步锁,一般搭配notify()或notifyAll()方法使用。

2、notify和notifyAll方法

notify()可以唤醒一个被wait的线程,notifyAll() 可以唤醒所有被wait()的方法,使它们进入就绪队列,以便在当前线程释放锁后竞争锁。

3、注意

这三个方法都不是定义在Thread中的方法,而是定义在Object中的方法。这三个方法必须使用在同步代码块或同步方法中,且这三个方法的调用者必须是同步代码块或者同步方法中的同步监视器。

七、其他

1、jdk5.0后新增的线程创建方式

jdk5.0之后又新增了两种线程创建的方式,Callable接口和ExecutorService(线程池)的方式,我会在后续的文章中详细介绍者两种方式。

2、ThreadLocal

线程局部变量,用于线程之间数据隔离。

3、ConcurrentHashMap

ConcurrentHashMap 是HashMap的一个线程安全的、支持高效并发的版本。