多线程(一)线程基础、线程之间的共享和协作

1.基础概念

CPU核心数和线程数的关系
核心数:线程数=1:1 ;使用了超线程技术后—> 1:2

CPU时间片轮转机制
又称RR调度,会导致上下文切换

什么是进程和线程
进程:程序运行资源分配的最小单位,进程内部有多个线程,会共享这个进程的资源
线程:CPU调度的最小单位,必须依赖进程而存在。(先有进程再有线程。CPU先将资源分配给进程,然后线程再使用资源)

澄清并行和并发
并行:同一时刻,可以同时处理事情的能力
并发:与单位时间相关,在单位时间内可以处理事情的能力

高并发编程的意义、好处和注意事项
好处:充分利用cpu的资源、加快用户响应的时间,程序模块化,异步化
问题:
线程共享资源,存在冲突;
容易导致死锁;
启用太多的线程,就有搞垮机器的可能

2.认识Java里的线程

2.1 创建线程的三种方式

  • 继承Thread类
  • 实现Runnable接口
  • 实现Callable接口
    Runnable和Callable的区别: Runnable接口中的run方法没有返回值,而Callable接口中的call方法有返回值
public class MyThread extends Thread {

    //实现Runnable接口
    private static class UseRun implements Runnable{

        @Override
        public void run() {
            System.out.println("I'm runnable implements");
        }
    }

    //实现Callable接口
    private static class UseCall implements Callable<String>{

        @Override
        public String call() throws Exception {
            System.out.println("I'm callable implements");
            return "callable";
        }
    }

    //测试
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        UseRun useRun = new UseRun();
        //实现Runnable接口的类 需要交给线程类Thread去执行
        new Thread(userRun).start();

       UseCall useCall = new UseCall();
       //PS:实现Callable的类无法直接转化为Thread 这就要使用FutureTask类 该类继承了Runnable接口
        //相当于将UseCall -> Runnable -> Thread
        FutureTask<String> futureTask = new FutureTask<>(userCall);
        //执行该线程
        new Thread(futureTask).start();
        //获取并且打印UserCall的返回值
        System.out.println(futureTask.get());
    }

}

2.2 如何让Java里的线程安全停止工作

①线程自然终止:②自然执行完或抛出未处理异常

2.2.1 已经过时的三种方法:stop(),resume(),suspend()

  • stop() :过于强硬会导致线程不会正确释放资源,
  • suspend() (挂起) :处于挂起状态,线程不会释放资源 当其他的线程需要资源时 会导致资源无法获取 这样就容易导致死锁。

2.2.2 现在推荐使用的三种方法

  • interrupt: 调用一个线程的interrupt() 方法中断一个线程,并不是强行关闭这个线程,只是跟这个线程打个招呼,将线程的中断标志位置为true,线程是否中断,由线程本身决定。
  • isInterrupted() 判定当前线程是否处于中断状态(即判断线程标志位是否为true)。
  • static方法interrupted() 判定当前线程是否处于中断状态,同时中断标志位改为false。
    方法里如果抛出InterruptedException,线程的中断标志位会被复位成false,如果确实是需要中断线程,要求我们自己在catch语句块里再次调用interrupt()。
  • interrupted()方法是静态的,底层调用了isInterrupted()方法,并且会将中断标志位重置。
public static boolean interrupted() {
    return currentThread().isInterrupted(true);
}

这三种方法只有Thread以及其子类才能够使用

2.2.3 代码演示

1.继承Thread类创建线程方式 安全中断线程的代码:

public class EndThread {

    private static class UseThread extends Thread{

        public  UseThread(String name){
            super(name);
        }

        //重写Thread中的run方法
        @Override
        public void run(){
            String name = Thread.currentThread().getName();
            //判断当前的线程是否被中断
            while(! isInterrupted()){
                //获取当前运行线程的名字
                System.out.println("Thread:" + name + " is run");
            }
            System.out.println("interrupt flag is : " + isInterrupted());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        //创建线程的实例对象
        Thread endThread = new UseThread("endThread");
        endThread.start();
        //令主线程休眠2秒 这样就不会立刻执行后面的interrupt()方法
        Thread.sleep(2000);
        endThread.interrupt();
    }
}

UseThread线程会运行2s 直到2s后main方法执行endThread.interrupt(); 线程中断。

执行结果:

Thread:endThread is run
Thread:endThread is run
Thread:endThread is run
Thread:endThread is run
Thread:endThread is run
Thread:endThread is run
interrupt flag is : true
  • 如果将run方法中的代码改为如下:
 @Override
        public void run(){
            String name = Thread.currentThread().getName();
            //判断当前的线程是否被中断
            while(true){
                //获取当前运行线程的名字
                System.out.println("Thread:" + name + " is run");
                System.out.println("interrupt flag is : " + isInterrupted());
            }
        }

执行结果为:

Thread:endThread is run
interrupt flag is : false
Thread:endThread is run
interrupt flag is : false
Thread:endThread is run
interrupt flag is : false

interrupt flag is : true
Thread:endThread is run
interrupt flag is : true
Thread:endThread is run
interrupt flag is : true
Thread:endThread is run

程序会永远执行下去。 可以看到2s之前中断标志一直为false,2s后运行到主方法的endThread.interrupt();后中断标志变为true 但程序仍然会一直运行,这恰恰说明了 java的线程是协作式的并不是抢占式的。即使发出了中断线程的信号,线程仍然执行,因为线程是否中断最终的决定权是在线程本身。

2.实现Runnable接口方式创建线程 安全中断线程的方法

其实该方法和上面的方法大同小异 只是不能直接使用interrupt()、isInterrupted()、interrupted()这三种方法

public class EndRunnable {

    private static class UseRunnable implements Runnable{

        @Override
        public void run() {
            String name = Thread.currentThread().getName();//❤
            //判断当前线程是否被中断 使用Thread.currentThread().isInterrupted() 是因为只有Thread类才有这个方法
            while (! Thread.currentThread().isInterrupted()){//❤
                //获取当前运行线程的名字
                System.out.println("Thread:" + name + " is run");
            }
            System.out.println("interrupt flag is : " + Thread.currentThread().isInterrupted());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread endRunnable = new Thread(new UseRunnable(), "useRunnable");//❤
        endRunnable.start();
        Thread.sleep(2000);
        endRunnable.interrupt();
    }
}

2.3如何处理线程抛出的InterruptedException异常

public class HasInterruptException {

    private static class UseThread extends Thread{

        public  UseThread(String name){
            super(name);
        }

        //重写Thread中的run方法
        @Override
        public void run(){
            String name = Thread.currentThread().getName();
            //判断当前的线程是否被中断
            while(! isInterrupted()){
                //获取当前运行线程的名字
                try {
                    System.out.println("Thread:" + name + " is run");
                    //制造中断异常 InterruptedException
                    Thread.sleep(200);//❤
                }catch (InterruptedException e){
       
                    //打印标志位
                    System.out.println("catch ----- interrupt flag is : " + isInterrupted());//❤
                    e.printStackTrace();
                }
                System.out.println("after catch ----- interrupt flag is : " + isInterrupted());//❤
            }
            System.out.println("after while ----- interrupt flag is : " + isInterrupted());//❤
        }
    }

    public static void main(String[] args) throws InterruptedException {
        //创建线程的实例对象
        Thread endThread = new UseThread("endThread");
        endThread.start();
        //令主线程休眠2秒 这样就不会立刻执行后面的interrupt()方法
        Thread.sleep(2000);
        endThread.interrupt();
    }
}

执行过程:线程在前2s会执行10次(在run方法中线程每执行一次会休眠200ms)
第2s时main方法执行到endThread.interrupt();会打断run方法中的休眠 这样就会抛出java.lang.InterruptedException: sleep interrupted异常。
然后会一直执行,永不停止。

执行结果:

Thread:endThread is run
after catch ----- interrupt flag is : false
Thread:endThread is run
catch ----- interrupt flag is : false
//以上为2s前线程运行情况

//线程发生异常
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at pers.amos.concurrent.safeend.HasInterruptException$UseThread.run(HasInterruptException.java:27)

after catch ----- interrupt flag is : false
Thread:endThread is run
after catch ----- interrupt flag is : false
Thread:endThread is run
after catch ----- interrupt flag is : false

可以看到在异常发生后中断标志重置为false 这样线程才能够一直执行
总结:当线程中发生InterruptedException异常时,标志位会被重置为false

如何改进程序,防止这类情况的发生?

catch中加入语句interrupt(); 对线程进行再次中断即可

 try {
         System.out.println("Thread:" + name + " is run");
         //制造中断异常 InterruptedException
         hread.sleep(200);
}catch (InterruptedException e){ 
         interrupt();//❤
         //打印标志位
         System.out.println("catch ----- interrupt flag is : " + isInterrupted());
         e.printStackTrace();
}

改进后的执行结果:

Thread:endThread is run
after catch ----- interrupt flag is : false
Thread:endThread is run
catch ----- interrupt flag is : true
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at pers.amos.concurrent.safeend.HasInterruptException$UseThread.run(HasInterruptException.java:27)
after catch ----- interrupt flag is : true
after while ----- interrupt flag is : true

★线程之间状态转换

多线程(一)线程基础、线程之间的共享和协作_第1张图片

1.yield和sleep方法的区别

  • sleep方法给其他线程机会时不考虑线程优先级(优先级低的也有可能);而yield方法只会给相同优先级或者更高优先级线程机会(如果没有相同或者更高优先级的线程,该线程会继续运行)
  • 线程执行sleep方法进入阻塞状态,执行yield方法进入就绪状态(也就是说线程执行sleep后再未被唤醒之前不会再次被CPU选中并执行任务,而yield仍有可能继续执行)

2.深入理解run()和start()

线程对象调用run()和调用start()的区别

public class StartAndRun {

    private static class ThreadRun extends Thread{

        @Override
        public void run() {
            int i = 5;
            while(i > 0){
                System.out.println("我是-" + Thread.currentThread().getName() + "-" + i );
                i --;
            }
        }
    }

    public static void main(String[] args) {
        ThreadRun threadRun = new ThreadRun();
        threadRun.setName("amosThread");
        //设置优先级
        threadRun.setPriority(1);
        threadRun.run();
        threadRun.start();
    }
}

输出结果:

我是-main-5
我是-main-4
我是-main-3
我是-main-2
我是-main-1
我是-amosThread-5
我是-amosThread-4
我是-amosThread-3
我是-amosThread-2
我是-amosThread-1

run()方法由谁去调用它就归属于哪个线程(这里是在主线程里调用的)其实和普通方法的调用是一样的。
start()方法:调用该方法后JVM会把线程类映射成一个线程。

3.线程的优先级:
取值为1~10,缺省为5,但线程的优先级不可靠,不建议作为线程开发时候的手段

 //设置优先级
threadRun.setPriority(1);

4.守护线程
和主线程共死,finally不能保证一定执行

public class DaemonThread {

   private static class UseThread extends Thread {

        @Override
        public void run() {

            while (!isInterrupted()) {
                System.out.println("Thread:" + Thread.currentThread().getName() + " is run");
            }
            System.out.println(Thread.currentThread().getName()
                    + " interrupt flag is " + isInterrupted());
        }
    }

1.当endThread 不设置为守护线程时,主线程执行完毕,endThread线程会永远执行下去。

    public static void main(String[] args) throws InterruptedException {
        //创建线程的实例对象
        Thread endThread = new UseThread();
        endThread.start();
        //令主线程休眠5毫秒 这样就不会立刻执行后面的interrupt()方法
        Thread.sleep(5);
    }
}

2.当endThread线程设置为守护线程时,主线程执行完毕,endThread线程也会停止,这就叫做与主线程共死。
PS:设置守护线程一定要在调用start()方法之前

    public static void main(String[] args) throws InterruptedException {
        //创建线程的实例对象
        Thread endThread = new UseThread();
	//设置线程为守护线程
 	endThread.setDaemon(true);//❤
        endThread.start();
        //令主线程休眠5毫秒 这样就不会立刻执行后面的interrupt()方法
        Thread.sleep(5);
    }
}

3.守护线程的finally不一定执行

public class DaemonThread {

    private static class UseThread extends Thread {

        @Override
        public void run() {
            try {
                while (!isInterrupted()) {
                    System.out.println("Thread:" + Thread.currentThread().getName() + " is run");
                }
                System.out.println(Thread.currentThread().getName()
                        + " interrupt flag is " + isInterrupted());
            } finally {
                System.out.println("finally.............");
            }
        }
    }


    public static void main(String[] args) throws InterruptedException {
        //创建线程的实例对象
        Thread endThread = new UseThread();
        //设置线程为守护线程
        endThread.setDaemon(true);
        endThread.start();
        //令主线程休眠5毫秒 这样就不会立刻执行后面的interrupt()方法
        Thread.sleep(5);
    }
}

执行结果:

Thread:Thread-0 is run
Thread:Thread-0 is run
Thread:Thread-0 is run
Thread:Thread-0 is run
Thread:Thread-0 is run
Process finished with exit code 0

为什么finally不会被执行?

java线程分为两类,守护线程和非守护线程。当所有的非守护线程中止时,不论存不存在守护线程,虚拟机都会kill掉守护线程从而中止程序。 虚拟机中,执行main方法的线程就是一个非守护线程,垃圾回收则是另一个守护线程,main执行完,程序就中止了,而不管垃圾回收线程是否中止。 所以,如果守护线程中存在finally代码块,那么当所有的非守护线程中止时,守护线程被kill掉,其finally代码块是不会执行的。

2.4 线程间的共享

2.4.1 synchronized内置锁

synchronized分为:对象锁和类锁

对象锁:private synchronized void instance2(){};锁住的方法没用static修饰

类锁:private static synchronized void synClass(){};锁住的方法使用了static修饰

作用:在多线程的环境下,控制synchronized代码段不被多个线程同时执行。synchronized既可以加在一段代码上,也可以加在方法上。

使用:synchronized锁住的是括号里的对象,而不是代码。对于非static的synchronized方法,锁的就是对象本身也就是this。
代码演示:

public class SynClzAndInst {
	
	//使用类锁的线程
    private static class SynClass extends Thread{
        @Override
        public void run() {
            System.out.println("TestClass is running...");
            synClass();
        }
    }

  //使用对象锁的线程
    private static class InstanceSyn implements Runnable{
        private SynClzAndInst synClzAndInst;

        public InstanceSyn(SynClzAndInst synClzAndInst) {
            this.synClzAndInst = synClzAndInst;
        }

        @Override
        public void run() {
            System.out.println("TestInstance is running..."+synClzAndInst);
            synClzAndInst.instance();
        }
    }

  //使用对象锁的线程
    private static class Instance2Syn implements Runnable{
        private SynClzAndInst synClzAndInst;

        public Instance2Syn(SynClzAndInst synClzAndInst) {
            this.synClzAndInst = synClzAndInst;
        }
        @Override
        public void run() {
            System.out.println("TestInstance2 is running..."+synClzAndInst);
            synClzAndInst.instance2();
        }
    }

    //对象锁的方法
    private synchronized void instance(){
        SleepTools.second(3);
        System.out.println("synInstance is going..."+this.toString());
        SleepTools.second(3);
        System.out.println("synInstance ended "+this.toString());
    }

    //对象锁的方法
    private synchronized void instance2(){
        SleepTools.second(3);
        System.out.println("synInstance2 is going..."+this.toString());
        SleepTools.second(3);
        System.out.println("synInstance2 ended "+this.toString());
    }

    //类锁,实际是锁类的class对象
    private static synchronized void synClass(){
        SleepTools.second(1);
        System.out.println("synClass going...");
        SleepTools.second(1);
        System.out.println("synClass end");
    }

例1.使用对象锁同时锁住两个不同的对象synClzAndInst和synClzAndInst2

public static void main(String[] args) {
        SynClzAndInst synClzAndInst = new SynClzAndInst();
        Thread t1 = new Thread(new InstanceSyn(synClzAndInst));

        SynClzAndInst synClzAndInst2 = new SynClzAndInst();
        Thread t2 = new Thread(new Instance2Syn(synClzAndInst2));

    	t1.start();
    	t2.start();
    	//让主线程休眠1s
    	SleepTools.second(1);
    }

使用对象锁锁住两个不同的对象,这两个线程t1和t2是并发执行的。
如果使用对象锁锁住同一个对象 很显然t1和t2线程是无法并发执行的。

执行结果:

synInstance is going...pers.amos.concurrent.syn.SynClzAndInst@16843df4
synInstance2 is going...pers.amos.concurrent.syn.SynClzAndInst@4bd8a307
synInstance ended pers.amos.concurrent.syn.SynClzAndInst@16843df4
synInstance2 ended pers.amos.concurrent.syn.SynClzAndInst@4bd8a307

例2.使用对象锁锁同一个对象synClzAndInst

public static void main(String[] args) {
        SynClzAndInst synClzAndInst = new SynClzAndInst();
        Thread t1 = new Thread(new InstanceSyn(synClzAndInst));

        Thread t2 = new Thread(new Instance2Syn(synClzAndInst));

    	t1.start();
    	t2.start();
    	//让主线程休眠1s
    	SleepTools.second(1);
    }

执行结果:

synInstance2 is going...pers.amos.concurrent.syn.SynClzAndInst@16843df4
synInstance2 ended pers.amos.concurrent.syn.SynClzAndInst@16843df4
synInstance is going...pers.amos.concurrent.syn.SynClzAndInst@16843df4
synInstance ended pers.amos.concurrent.syn.SynClzAndInst@16843df4

两个线程InstanceSyn和Instance2Syn分别会调用对象锁方法instance和instance2 如果锁住的是同一个对象,两个线程只能有一个线程获得锁 并且执行instance和instance2其中一个方法。

类锁 锁住的是每个类的class对象 该类的对象由JVM保证是唯一的。所以使用类锁时,只会有一个线程获得锁。

例3.使用类锁 锁住两个不同的对象 只能有一个线程获得锁并且执行

public static void main(String[] args) {
       /* SynClzAndInst synClzAndInst = new SynClzAndInst();
        Thread t1 = new Thread(new InstanceSyn(synClzAndInst));

        Thread t2 = new Thread(new Instance2Syn(synClzAndInst));*/
        SynClass synClass1 = new SynClass();
        SynClass synClass2 = new SynClass();
        synClass1.start();
        synClass2.start();
    	//让主线程休眠1s
    	SleepTools.second(1);
    }

执行结果:

TestClass is running...
TestClass is running...
synClass1 going...
synClass1 end
synClass2 going...
synClass2 end

总结:使用synchronized对 对象加锁时 如果是对象锁 应注意加到同一个实例对象上(如例2)而不是加到不同的对象上(如例1)

2.4.2 volatile关键字

  • volatile关键字的用处:
//在类中定义属性
private volatile int age = 100000;//初始100000

volatile关键字是线程不安全的 只能保证可见性不能保证age的原子性
当使用get方法获取age的值时,会强制从主存中读取age的值,无视当前线程中缓存的age的值
当使用set方法设置age的值时,会强制将该值刷新到主存中去。

  • 为什么volatile不能保证age的原子性?
    假如age的set方法如下:
public void setAge() {
     age = age+20;
}

加法不是一个原子操作 JVM和操作系统会执行多条指令才能完成加法操作

用代码验证volatile关键字不能保证原子性:

public class VolatileUnsafe {

    private static class VolatileVar implements Runnable{

        private volatile int a = 0;

        @Override
        public void run() {
            a = a + 1;
            System.out.println(Thread.currentThread().getName() + "==============" + a);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            a = a + 1;
            System.out.println(Thread.currentThread().getName() + "==============" + a);
        }

    }

    public static void main(String[] args) {
        VolatileVar volatileVar = new VolatileVar();
        Thread t1 = new Thread(volatileVar);
        Thread t2 = new Thread(volatileVar);
        Thread t3 = new Thread(volatileVar);
        Thread t4 = new Thread(volatileVar);
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

代码执行结果:

Thread-0==============2
Thread-2==============3
Thread-1==============2
Thread-3==============4
Thread-3==============5
Thread-2==============6
Thread-1==============5
Thread-0==============7
理想结果应为从1-8顺序输出

总结:volatile的应用场景:适合于只有一个线程写,多个线程读的场景,因为它只能确保可见性。

2.4.3 线程变量ThreadLocal

线程变量。可以理解为是个map,类型 Map
保证每个线程只会使用自己那一份变量的拷贝 各个线程对同一个变量的修改不会相互影响

public class UseThreadLocal {
	
	//可以理解为 一个map,类型 Map
	static ThreadLocal<Integer> threadLaocl = new ThreadLocal<Integer>(){
		@Override
		protected Integer initialValue() {
			return 1;
		}
	};

    /**
     * 运行3个线程
     */
    public void StartThreadArray(){
        Thread[] runs = new Thread[3];
        for(int i=0;i<runs.length;i++){
            runs[i]=new Thread(new TestThread(i));
        }
        for(int i=0;i<runs.length;i++){
            runs[i].start();
        }
    }
    
    /**
     *类说明:测试线程,线程的工作是将ThreadLocal变量的值变化,并写回,看看线程之间是否会互相影响
     */
    public static class TestThread implements Runnable{
        int id;
        public TestThread(int id){
            this.id = id;
        }
        public void run() {
            System.out.println(Thread.currentThread().getName()+":start");
            Integer s = threadLaocl.get();//获得变量的值
            s = s+id;
            threadLaocl.set(s);
            System.out.println(Thread.currentThread().getName()+":"
            +threadLaocl.get());
            //threadLaocl.remove();
        }
    }

    public static void main(String[] args){
    	UseThreadLocal test = new UseThreadLocal();
        test.StartThreadArray();
    }
}

代码执行结果:

Thread-0:start
Thread-1:start
Thread-2:start
Thread-1:2
Thread-0:1
Thread-2:3

一般ThreadLocal在线程池使用,每一个线程都相互隔离。保证了线程之间不会因共享的同一个变量而出现冲突。

你可能感兴趣的:(多线程)