JavaEE之线程篇(初阶版)

一、线程的引入

在os中引入线程的目的是减少程序在并发执行时所付出的时空开销,使os具有更好的并发性

时空开销:

1.创建进程,要为之分配所必需的,除处理机之外的所有资源

2.撤销进程,要回收其所占的资源,在撤销PCB

3.进程切换,对进程进行上下文切换时,需要保留当前进程的cpu环境,设置新选进程的cpu环境

二、线程的概念

线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务

三、进程与线程的区别

1.根本区别:进程是操作系统资源分配和独立调度的基本单位,而线程是处理器任务调度和执行的基本单位

2.资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。

3.包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。

4.内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的

5.影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。

6.执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行

7.支持多处理机系统:传统的进程,即单线程进程,不管有多少处理机,该进程只能运行在一个处理机上,但是对于多线程进程,就可以把一个进程中的多个线程分配到多个处理机上运行,使他们并行执行,加快进程的完成

四、Java中Thread类的基本使用

(一).线程的创建方法

1.创建类继承Thread重写run方法

public class MyThread extends Thread{
    //run里面的逻辑就是线程要执行的工作,创建一个线程并安排执行的任务
    @Override
    public void run() {
        System.out.println("hello thread");
    }

    //演示线程的基本创建
    public static void main(String[] args) {
        //程序中有两个线程 1、main主进程自带的线程(是JVM创建的,main方法是线程的入口)
        //2、myThread创建的新线程(run方法是新线程的入口)
        MyThread myThread = new MyThread();
        //另外启动的一个线程,开始执行run方法,run方法结束后代表该线程结束
        //该新线程是一个单独的执行流,与已有的主线程main是不干扰的,是并发(并发+并行)执行的
        myThread.start();//只有调用start才代表开始启动线程,在操作系统内核中才会有一个与该线程对应
    }
}

2.创建一个类,实现Runnable接口,再创建Runnable是实例传给Thread的构造方法

public class MyRunnable implements Runnable {
    //runnable是用来表述线程要执行的具体操作,仅仅是把线程和线程要执行的工作分开,目的降低耦合度
    @Override
    public void run() {
        while(true){
            System.out.println("hello thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        //创建一个runnable实例,runnable是描述具体的操作
        MyRunnable myRunnable = new MyRunnable();
        //创建一个线程,把runnable放进去
        Thread thread = new Thread(myRunnable);
        thread.start();
        //多个线程执行同样的工作
        Thread thread2 = new Thread(myRunnable);
        thread2.start();

        while (true) {
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

3.使用匿名内部类创建线程,同时重写run方法

public class MyThread3 {
    //使用匿名内部类创建线程
    public static void main(String[] args) {
        //创建一个Thread的子类,同时实例化对象
        Thread thread = new Thread(){
            @Override
            public void run() {
                while(true) {
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        thread.start();
    }
}

4.使用匿名内部类实现Runnable接口,同时new出的Runnable实例传给Thread的构造方法

public class MyThread4 {
    //使用匿名内部类实现Runnable接口
    public static void main(String[] args) {
        //匿名内部类的实例作为构造方法的参数,放在Thread的圆括号里面,创建一个线程实例
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                while(true) {
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        thread.start();
    }
}

5.使用lambda表达式

public class MyThread5 {
    //使用lambda表达式(本质上是一个匿名内部类)
    public static void main(String[] args) {
        //是MyThread的实现操作的间歇
        Thread thread = new Thread(()->{
            while(true) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread.start();
    }
}

(二).线程的其他操作 

1.给线程起名字(便于多线程的调试)

public class MyThread6 {
    //给线程起名字->方便调试
    public static void main(String[] args) {
        //thread是变量名  只存在于代码中,而线程名既存在于代码中,又存在于执行的程序中
        Thread thread = new Thread(()->{
            while(true) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"MyThread777");
        thread.start();
    }
}

2.线程的join方法

//线程等待(线程的调度顺序是未知的)
//通过join控制线程的之间的结束顺序(阻塞就是让线程不参与CPU的调度)
//join还可以设置为带时间的版本,表示等待几秒,如果超过了等待的时间,就不等了,就开始和main的线程并发执行了
//join不带时间的版本,就是一直等待,直到调用join方法的线程的逻辑执行完
public class MyThreadJoin {
    public static void main(String[] args) {
        Thread thread = new Thread(()->{
            for (int i = 0; i < 5; i++) {
                System.out.println("hello thread");
                try {
                    //让线程休息,其实就是阻塞线程,不让线程参与CPU的调度
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } 
        });
        thread.start();

        System.out.println("hello main 在join之前");

        try {
            //main线程进入阻塞不参与调度,而thread参与调度
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("hello main 在join之后");
    }
}

3. 设置后台线程

public class MyThread7 {
    //线程分为前台线程和后台线程
    //关系是->只要有前台线程没有结束,程序就会一直进行,只要前台线程没有了,不管后台线程,程序直接结束
    //线程默认是前台线程
    public static void main(String[] args) {
        Thread thread = new Thread(()->{
            while(true) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"MyThread777");
        //setDaemon把线程设置为后台线程
        thread.setDaemon(true);

        thread.start();
        try {
            //阻塞main线程,直到hello thread结束,在执行main线程
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("main线程结束了");

//结果1是(没有使用join方法)->main线程结束了+hello thread,程序就结束了,因为main线程结束了,而thread是后台线程,(自己可以去掉join看结果)
//结果2是一直打赢hello thread,因为有join(),使用main线程要等到thread结束才能结束main线程
// 代表此时没有前台线程了,程序就结束
    }
}

4.查看线程的相关属性

public class MyThread8 {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            while (true) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "MyThread888");
        thread.start();
        System.out.println(thread.getId());
        System.out.println(thread.getName());
        System.out.println(thread.getPriority());
        System.out.println(thread.getState());
        System.out.println(thread.isDaemon());
        System.out.println(thread.isAlive());
        System.out.println(thread.isInterrupted());
    }
    //12
    //MyThread888
    //5
    //hello thread
    //RUNNABLE
    //false
    //true
    //false
}

5. 线程之间的抢占

class MyThread99 extends Thread {
    //run里面的逻辑就是线程要执行的工作,创建一个线程并安排执行的任务
    @Override
    public void run() {
        while(true) {
            System.out.println("hello thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class MyThread9 {
    public static void main(String[] args) {
        MyThread99 myThread99 = new MyThread99();
        //myThread99.start();//创建新线程,在新线程中执行代码,和main的线程并发执行  结果->hello thread和hello main"不规则"的出现
        myThread99.run();//直接调run并没有创建线程,只是在main的线程中运行  结果->只出现hello thread(因为没有创建新线程,抢占了main线程的执行)
        while(true) {
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

五、Java中线程的几种状态

1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。

2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。

3. 阻塞(BLOCKED):表示线程阻塞于锁。

4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。

5. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。

6. 终止(TERMINATED):表示该线程已经执行完毕。

JavaEE之线程篇(初阶版)_第1张图片

状态详细说明
1. 初始状态(NEW)
实现Runnable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了初始状态。

2. 就绪状态(RUNNABLE之READY)
就绪状态只是说你资格运行,CPU的调度程序没有挑选到你,你就一直是就绪状态。调用线程的start()方法,此线程进入就绪状态。当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。锁池里的线程拿到对象锁后,进入就绪状态。
3. 运行中状态(RUNNABLE之RUNNING)
线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一的一种方式。

4. 阻塞状态(BLOCKED)
阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。

5. 等待(WAITING)
处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。

6. 超时等待(TIMED_WAITING)
处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。

7. 终止状态(TERMINATED)
当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。
在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。

六、线程安全引起的原因和解决

1.什么是线程安全?

如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。

2.线程安全的例子

a.使用synchronized解决

//线程安全的基本使用
//1.使用synchronized修饰方法,进入方法加锁,方法执行完,自动解锁
class Counter{
    private int count = 0;

    public synchronized void increase(){
        count++;
    }
    public int getCount(){
        return count;
    }
}
public class Demo {
    private static Counter counter = new Counter();

    public static void main(String[] args) throws InterruptedException {
        //搞t1和t2线程,每个线程自增1w次,预期结果是2w

        Thread thread1 = new Thread(()->{
            for (int i = 0; i < 1_0000; i++) {
                counter.increase();
            }
        });
        Thread thread2 = new Thread(()->{
            for (int i = 0; i < 1_0000; i++) {
                counter.increase();
            }
        });
        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        System.out.println(counter.getCount());

    }
    //锁拥有独占特性,意思是当前没有人去加锁,尝试加锁的线程会成功
    //但是如果当前锁有线程使用,其他线程要加锁,只能等待该锁解除
    //synchronized会把并行变成串行,执行速度降低,但是准确性提高
    //加锁不代表cpu会直接把线程t1执行完再去执行其他的,根据调度策略会切换,但是即使t1被调度走了,而锁依然没有释放,t2还是不能使用
    //线程安全不是加锁就安全,而是通过加锁,让并发带来的问题操作变成串行操作
}
//increase加锁,increase2不加搜
class Counter2{
    private static int count = 0;

    public synchronized void increase(){
        count++;
    }
    public void increase2(){
        count++;
    }
    public int getCount(){
        return count;
    }
}
public class Demo2 {
    private static Counter2 counter = new Counter2();

    public static void main(String[] args) throws InterruptedException {
        //搞t1和t2线程,每个线程自增1w次,预期结果是2w

        Thread thread1 = new Thread(()->{
            for (int i = 0; i < 1_0000; i++) {
                counter.increase();
            }
        });
        Thread thread2 = new Thread(()->{
            for (int i = 0; i < 1_0000; i++) {
                counter.increase2();
            }
        });
        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        System.out.println(counter.getCount());
    }
    //t1加锁,t2不加锁,不会出现锁竞争的情况,就不能把并行的问题的操作变成串行操作
}
//2.synchronized修饰代码块
class Counter3{
    private static int count = 0;

    public Object locker = new Object();
    public void increase(){
        //括号里要填入加锁的对象(任意对象)
        //明确对哪个对象进行加锁,会影响是否触发阻塞(对象要根据使用场景而言)
        //一个synchronized只能锁一个对象
        synchronized(this){
            //锁this等价于锁方法
            //针对当前对象进行加锁,谁调用increase方法谁就是当前锁对象
            count++;
        }
//        synchronized(locker){
//            //针对locker加锁,如果创建的对象中有一样的locker实例,那就是针对同一对象加锁(产生锁竞争)
//            count++;
//        }
    }
    public int getCount(){
        return count;
    }
}
public class Demo3 {
    private static Counter3 counter = new Counter3();

    public static void main(String[] args) throws InterruptedException {
        //搞t1和t2线程,每个线程自增1w次,预期结果是2w

        Thread thread1 = new Thread(()->{
            for (int i = 0; i < 1_0000; i++) {
                counter.increase();
            }
        });
        Thread thread2 = new Thread(()->{
            for (int i = 0; i < 1_0000; i++) {
                counter.increase();
            }
        });
        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        System.out.println(counter.getCount());
    }
    //两个线程锁必须同一个对象,才会产生锁竞争现象
}

b.使用volatile关键字

import java.util.Scanner;

//volatile保证内存可见性,不保证原子性
public class Demo9 {
    static class Counter{
        //volatile只能去修饰变量,无法修饰其他的东西
        public volatile int count = 0;
        //volatile修饰的变量会让编译器不去优化改变量为只读寄存器而不读内存
    }
    public static void  main(String[] args) {
        int a = 1;
        Demo9.Counter counter = new Demo9.Counter();
        Thread thread = new Thread(()->{
           while(counter.count == 0){

           }
            System.out.println("t1执行结束");
        });
        thread.start();
        Thread thread2 = new Thread(()->{
            System.out.println("请输入一个整数:");
            Scanner scanner = new Scanner(System.in);
            counter.count = scanner.nextInt();
        });
        thread2.start();
    }
    //死循环的原因:t2线程的写操作对t1的读操作不会有影响,因为t1的操作已经被优化成了读1次count的内容就不在读了(内存可见性问题,没有加volatile)
    //内存可见性:在编译器的优化下(因为count的值是不变的,编译器就优化了),一个线程把内存的值改了,因为优化的原因,另一个线程无法感知到改值已修改
    //编译器的优化不是一直优化的,要取决于当前的环境因素(可以给代码加上sleep去看)
}

以上就是对初阶线程以及涉及的相关问题的讲解,欢迎大家的指教,请大家耐心等待后序的进阶线程的讲解.

你可能感兴趣的