本章包含知识点:并发简单概念,线程的简单创建与使用,线程的休眠,线程的生命周期,线程优先级,线程礼让,线程加入。
进程:一个程序运行所占用的资源的描述,一个程序被运行,系统就会为他开辟一个进程,是资源分分配的最小单位。进程之间不可相互通信。
线程:一个进程中有多个线程,在一个进程中可以建立新的现成线程是程序执行时的最小单位。进程包含线程,如果进程中的所有线程都没有了,进程就结束了,同一个线程内的进程何以共享信息!
我们在这里几个例子:
这里有一堆砖头,一堆沙子,一池子水,老板让你把这些都移走。
串行就是:无论先移动哪一个才,一次只能一个东西,等待这个东西全部搬完才可以移动下一个。先搬砖头,就不能搬沙子和水,等砖头搬完了才可以般沙子和水。即:一次只能执行一个任务!
并发就是:你搬一块砖头,铲一铲沙子,倒一桶水,然后这个时候就像你在同时办这两件事情一样。再举个例子就是吃一口饭,喝一口水,看一会电视,就像同时在做三件事情一样。但实际上,你一次只能做一件事,只是在所有事情之间快速切换执行,好像所有事情同时执行一样!
并行就是:老板让你干活,你直接又找了两个人,三个人一个人搬水,一个人搬砖,一个人搬沙子,是真正意义上的同时执行!
电脑执行程序:
其实在电脑内,执行多线程任务任务时,是并行合并发同时存在的!
单核处理器处理多线程任务即为并发,多核处理器执行任务为并行,而电脑中,是多核处理器同时执行更多的任务,每一个核心执行一个任务只执行一点,然后处理器的执行能力被其他任务抢走。只不过处理器切换执行任务的时间非常快,我们感觉像是同时执行一样!
CPU时间片:
是分时操作系统分配给每个正在运行的进程微观上的一段CPU时间。程序执行,就是每个程序抢时间片,抢到时间片程序就会执行一点,每个时间片的长短并不相同,需要根据程序执行情况看。
线程开辟的方式有两种:
演示1:继承Thread类重写run方法
package com.wojiushiwo;
/**
* @author 我就是我500
* @date 2020-02-22 20:40
* @describe
**/
public class ThreadTest {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
for(int i=0;i<10;i++)
{
System.out.println("主线程执行的逻辑!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class MyThread extends Thread
{
@Override
public void run() {
for(int i=0;i<10;i++)
{
System.out.println("自定义线程执行的逻辑!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
我们程序中的代码是按照顺序执行的,而我们的程序是交替运行的,说明线程已经成功创建并运行!
演示2:实现Runnable接口,重写其中run方法
package com.wojiushiwo;
/**
* @author 我就是我500
* @date 2020-02-22 20:40
* @describe
**/
public class ThreadTest {
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.start();
for(int i=0;i<10;i++)
{
System.out.println("主线程执行的逻辑!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class MyThread implements Runnable
{
@Override
public void run() {
for(int i=0;i<10;i++)
{
System.out.println("自定义线程执行的逻辑!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
还可以将显示实现Runnable接口方式改为使用lamda表达式方式:
package com.wojiushiwo;
/**
* @author 我就是我500
* @date 2020-02-22 20:40
* @describe
**/
public class ThreadTest {
public static void main(String[] args) {
Thread thread = new Thread(()->{
for(int i=0;i<10;i++)
{
System.out.println("自定义线程执行的逻辑!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
for(int i=0;i<10;i++)
{
System.out.println("主线程执行的逻辑!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
可以看到:两次执行虽然后是成功的但是结果并不相同,这是因为线程的本质是并发,是多个线程争抢CPU时间片的结果,而CPU时间片的争抢是随机的,也就是说不一定什么时候正在执行那条线程,所以造成了上方情况,下一章将对此详细介绍。
那么两种方法如何选择:
继承Thread类:优点是简单,直接实例化继承后的类调用strat方法即可,缺点是由于JAVA的单继承性,无法再继承其它父类!
实现Runnable接口:优点是不需要继承Thread类,并且可是使用lamda表达式语法,但是需要实例化Thread类,将Runnable接口作为参数传入。
以上两种方法都可以使用,但是实际开发中,更推荐使用第二种方式(实现Runnable接口)!
线程休眠方法:
Thread.Sleep(休眠时间);
此方法会阻塞线程的运行,使线程从运行态变为阻塞态,当休眠时间超过指定时间,此线程就会被重新唤醒。
如上方代码所示,使用线程sleep方法可以使线程暂停一部分时间,但是此方法会抛出异常,所以必须使用try/catch代码块捕捉异常,并且线程休眠后不会释放锁的标记,属于“占着茅坑不拉屎的类型”,使用时必须注意!
关于线程锁的问题,有下一章笔记行详细介绍。
线程从创建到销毁一共有5种状态:
等待队列和锁池中的线程对象不严格来说也属于阻塞态,接下来会继续介绍。
我们开启多个线程时,有时就需要区分这几个线程,这是我们可以在线程运行之前给线程命名。
获取当前线程名:
Thread.currentThread().getName();
我们分别来获取一下主线程和我们手动新建的线程的默认线程名:
public class ThreadTest {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName());
new Thread(()->{
System.out.println(Thread.currentThread().getName());
}).start();
}
}
这时候我们就可以看到:主线程默认名称为main,自定义线程中的线程名默认为Thread-数字的格式。
设置线程名的方式:
我们假设一个场景,目前有两条自定义线程正在工作并输出结果,我们这是通过获取线程名的方式来判断到底是那一条线程输出了什么结果。
package com.wojiushiwo;
/**
* @author 我就是我500
* @date 2020-02-22 20:40
* @describe
**/
public class ThreadTest {
public static void main(String[] args) {
MyThread myThread =new MyThread();
Thread thread1 = new Thread(myThread,"线程1");
Thread thread2 = new Thread(myThread);
thread2.setName("线程2");
thread1.start();
thread2.start();
}
}
class MyThread implements Runnable
{
@Override
public void run() {
for (int i=0;i<10;i++)
{
System.out.println(Thread.currentThread().getName()+"输出了"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
我们可以看到,执行同样的代码,可以通过线程名来区分线程的工作结果。
结合上述代码,我们发现,有两种方式可以给线程命名:
至于使用哪种命名方式,根据情况而定即可!
查看上方的执行结果,我们不难发现,我们都是每执行一次循环逻辑就让他延迟(休眠)1秒,正常来说应该线程1和线程2交替执行,但是我们测试却发现在有些情况下,如红圈所示,两条线程会很杂乱的执行,这是因为我们的线程执行的本质是CPU在各个线程之间快速切换执行,而每个线程都需要抢到CPU时间片才可以执行,而什么时候抢到时间片是不确定的,所以如果某一个线程抢不到时间片,就会出现延后执行的情况下,所以如图红圈所示,我们会看到两条线程杂乱的执行,而不是顺序执行。
线程需要抢到CPU时间片才能执行,那么抢到时间片的概率就是我们的线程优先级,线程优先级高,就有更大的几率率先先执行完毕!
设置线程的优先级:
使用线程对象的setPriority()方法来设置线程优先级
注意:
现在我们分别设置两条线程的优先级进行实实验:
package com.wojiushiwo;
/**
* @author 我就是我500
* @date 2020-02-22 20:40
* @describe
**/
public class ThreadTest {
public static void main(String[] args) {
MyThread myThread =new MyThread();
Thread thread1 = new Thread(myThread,"线程1");
Thread thread2 = new Thread(myThread,"线程2");
thread1.setPriority(10);
thread2.setPriority(1);
thread1.start();
thread2.start();
}
}
class MyThread implements Runnable
{
@Override
public void run() {
for (int i=0;i<100;i++)
{
System.out.println(Thread.currentThread().getName()+"输出了"+i);
}
}
}
可以看到,线程1的优先级最高,线程2的优先级最低,在开始阶段两条线程交替执行,而且线程1的执行速度明显大于线程2,线程1执行完毕,线程2才执行一半!
第二次进行测试,同样是优先级更高的线程1率先执行完毕,但可以看到这次线程2才执行到6,所哟我们也可以得出结论,优先级只是线程抢到CPU时间片的概率,并不是一定最先执行,并不是每一次执行结果都相同。
使线程放弃已经抢到的时间片,从运行态回到就绪态的操作
也就是说:当前线程抢到CPU时间片之后,线程礼让会使当前线程放弃已经抢到的时间片,不执行当前逻辑,所有线程重新抢夺CPU时间片就是线程礼让!
注意:假如有两个线程,线程1执行了礼让操作后,并不一定线程2一定执行操作,因为线程礼让是让线程1放弃CPU时间片后所有线程重新抢夺,如果此时依旧是线程1抢到CPU时间片,依旧是线程1继续执行!
线程礼让代码:
Thread.yield();
线程礼让示例:我们使循环执行到20的时候进行礼让操作
package com.wojiushiwo;
/**
* @author 我就是我500
* @date 2020-02-22 20:40
* @describe
**/
public class ThreadTest {
public static void main(String[] args) {
MyThread myThread =new MyThread();
Thread thread1 = new Thread(myThread,"线程1");
Thread thread2 = new Thread(myThread,"线程2");
thread1.start();
thread2.start();
}
}
class MyThread implements Runnable
{
@Override
public void run() {
for (int i=0;i<100;i++)
{
if(i==20)
Thread.yield();
System.out.println(Thread.currentThread().getName()+"输出了"+i);
}
}
}
可以看到,线程2执行到20时,进行了礼让,所以线程1抢到了CPU时间片输出了结果,线程1在执行到20时,也进行了礼让操作,然而由于他运气好,再一次抢到了CPU时间片,所以依旧是线程1继续输出!所以,线程礼让并不一定会使其它线程进行执行!
线程加入就是吧同时执行的线程变为顺序执行!
线程加入的方法:线程对象.join();
join两种形式:
举例说明:join方法常用于在主线程中等待其它线程执行完成
package com.wojiushiwo;
/**
* @author 我就是我500
* @date 2020-02-22 20:40
* @describe
**/
public class ThreadTest {
public static void main(String[] args) {
MyThread myThread =new MyThread();
Thread thread1 = new Thread(myThread,"线程1");
thread1.start();
try {
thread1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i=0;i<10;i++)
{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程输出了"+i);
}
}
}
class MyThread implements Runnable
{
@Override
public void run() {
for (int i=0;i<10;i++)
{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"输出了"+i);
}
}
}
可以看到main线程中加入了线程1,原本应该同时执行的两条线程出现了等待,等待线程1执行结束后main线程才继续执行!
如果将join方法中加入参数:
thread1.join(3000);
可以看到main线程中加入了线程1,主线程等待了三秒,之后才交替执行。