day11【多线程】、继承、Runnable 接口、volatile、原子性

day11【多线程】

反馈和复习
1.throwthrows
    格式:
		throw new XxxException("异常描述信息");
		public static void 方法名()throws XxxException{
            
        }
	含义:
		throw new XxxException("异常描述信息"); 真的真的真的抛出了一个异常
        public static void 方法名()throws XxxException{}; 方法内部可能抛出异常(也可能不抛出)
    案例:
		 public static void readFile(String name) throws FileNotFoundException { 
            //假设硬盘上有一个叫1.txt的文件
            if ("1.txt".equals(name)) {
                //可以读
                System.out.println("读取文件成功...");
            }else{
                //抛出异常
                throw new FileNotFoundException("没有找到你要的文件:"+name);
            }
        }
 2.运行时和编译时异常区别
     继承:
		编译时继承Exception
        运行时继承RuntimeException
     使用:
		当出现运行时异常时,我们不需要throws,也不需要trycatch
        当出现编译时异常时,我们必须throws或者trycatch
     案例:
		 public static int getElement(int[] arr)throws ArrayIndexOutOfBoundsException{
            //自己判断,数组是否有3索引
            if (arr.length < 4) {
                //抛出异常
                throw new ArrayIndexOutOfBoundsException("哥们越界了!!!");
            }
            //获取数组中索引为3的元素
            int num = arr[3];
            //返回num
            return num;
        }

		调用方法:
		public static void main(String[] args) {
        	int[] arr = {1,2,3};
        	getElement(arr);
        }
3.在开发异常的使用
    a.如果没有遇到异常,该怎么写就怎么写代码
    b.如果遇到异常
    	如果编译时异常,要么throws要么trycatch
    	如果运行时异常,编译时不需要处理,运行后根据打印的异常信息,修改代码再次运行直到运行成功为止    
今日内容
今日主要是理解为主(因为今天内容很抽象)
多线程
1.如何使用多线程【掌握】 
2.高并发和线程安全问题【理解】  
3.volatile的作用【理解】
4.原子类的作用【理解】        

第一章 多线程【重点】

1.1 并行和并发【理解】
并行: 两个事件,在同一个时刻,都在发生
并发: 两个事件,在同一个时间段内,都在发生(交替执行)
    
案例:
	有一个111期同学,觉得课间10分钟,抽一根抽烟不爽,但是抽两个烟时间来不及
    a.并行抽烟, 点两根一起放嘴里抽
    b.并发抽烟, 点两根,这根烟抽一口,那根烟抽一口    

在这里插入图片描述

1.2 进程和线程【理解】
进程: 正在内存中运行的程序,我们称为进程
线程: 进程中完成某个小功能的模块(进程中用执行某个功能的执行单元) 
  • 进程和线程的一些区别【了解】

    线程是属于某个进程的
      	每个进程都有独立的内存空间(独立的栈独立的堆等),并且至少有一个线程
      	每个线程都会跟进程申请一块独立栈,共享进程的堆  
    
  • 线程调度【了解】

    线程调用是指CPU在不同的进程不同的线程之间进行快速切换
        
    线程调度的分类:
    	分时调度: 每个线程平均拥有CPU的执行权
    	抢占式调用: 每个线程随机分配CPU的执行权(具体的分配多少和线程优先级有关)
        我们Java程序(Java进程)中所有线程采用抢占式调度    
    
1.3 Thread类的介绍【重点】
我们右键运行的是一个Java进程,该进程中默认至少会有2个线程:
	a.main方法所在线程,称为主线程
    b.垃圾回收器线程  
我们是否可以再创建一个新的线程呢????
     可以!! 非常简单,因为Java已经把代表线程的类封装好了,Thread类
a.Thread类是什么?
    Thread是Java定义好的,代表线程的类,只要创建该类的一个对象,其实就是创建了一个线程
   
b.Thread类的构造方法
    public Thread(); // 无参构造,线程会有默认的名字,Thread-0,Thread-1等...
	public Thread(String name); //带有线程名字的构造

	public Thread(Runnable r);//带有线程任务的构造
	public Thread(Runnable r,String name); //即带有线程名字,又带有线程任务的构造
    
c.Thread类的成员方法
    public String getName(); //获取线程的名字
	public void setName(String name);//修改线程的名字

	public void run();//代表线程要执行的任务,任务有关的代码需要写在次方法中
	public void start();//线程只创建并不会执行,必须调用start开启后才会执行任务

	public static void sleep(long millis); //让当前线程"休眠/暂停"多少毫秒
			这里的当前线程只指 Thread.sleep(1000)这句代码写哪个线程中,哪个线程就是当前线程
	public static Thread currentThread();//获取当前线程对象
			这里的当前线程是指 Thread.currentThread() 这句代码写哪个线程中,哪个线程就是当前线程
1.4 创建新的线程方式一_继承方式【重点】
a.API描述:
	将类声明为 Thread 的子类。该子类应重写 Thread 类的 run 方法。接下来可以分配并启动该子类的实例
b.分析创建的步骤:
	i.创建子类 继承 Thread
    ii.子类中重写run方法(在run中编写线程要执行的任务代码)
    iii.创建子类对象(实际上就是创建一个线程对象)
    iv. 调用线程对象的start方法(启动该线程)    
c.案例:
	//i.创建子类 继承 Thread
    public class MyThread extends Thread {
        //ii.子类中重写run方法(在run中编写线程要执行的任务代码)
        @Override
        public void run() {
            for (int i = 0; i < 50; i++) {
                System.out.println("子线程..."+i);
            }
        }
    }
	public class ThreadDemo02 {
    public static void main(String[] args) {
        // iii.创建子类对象(实际上就是创建一个线程对象)
        MyThread mt = new MyThread();
        //iv. 调用线程对象的start方法(启动该线程)
        mt.start();
        //主线程 不会等待子线程任务结束
        for (int i = 0; i < 50; i++) {
            System.out.println("主线程..."+i);
        }
    }
}

注意:
	a.我们可以给线程起名字,也可以使用默认的名字
    b.我们获取线程的名字时:
			建议使用通用方式: Thread.currentThread().getName();
			如果是子线程内部也可以直接调用getName()获取子线程的名字

1.5 创建新的线程方式二_实现方式【重点】
a.API描述
    声明实现 Runnable 接口的类。该类然后实现 run 方法。
    然后可以分配该类的实例,在创建 Thread 时作为一个参数来传递	并启动.
b.分析步骤:
	i.创建实现类 实现 Runnable接口(实际上接口中一个任务方法,run方法)
    ii.实现类重写run方法(run中编写具体的任务代码)
    iii.创建实现类对象(该实现类对象并不是线程对象,我们称为任务对象) 
    iv. 创建Thread对象,同时传入实现类对象
        public Thread(Runnable r);//带有线程任务的构造
	v. 启动该线程(调用线程对象的start方法)
c.代码实现
    //i.创建实现类 实现 Runnable接口(实际上接口中一个任务方法,run方法)
    public class MyRunnable implements Runnable {
        //ii.实现类重写run方法(run中编写具体的任务代码)
        @Override
        public void run() {
            //run中写任务代码
            for (int i = 0; i < 50; i++) {
                System.out.println("子线程..."+i);
            }
        }
    }
    
	public class TestThread {
        public static void main(String[] args) {
            //iii.创建实现类对象(该实现类对象并不是线程对象,我们称为任务对象)
            MyRunnable mr = new MyRunnable();
            //iv. 创建Thread对象,同时传入实现类对象
            Thread tt = new Thread(mr);
            //v. 启动该线程(调用线程对象的start方法)
            tt.start();

            //主线程不会等待子线程执行完毕
            for (int i = 0; i < 50; i++) {
                System.out.println("主线程..."+i);
            }
        }
    }

1.5.1 两种方式的优劣比较
两种创建线程的方式,实现方式比较好
    a.实现方式比较好,因为实现方式线程和任务是分开,是由程序员自己组合
    b.实现方式避免了Java单继承不足
    c.实现方式线程和任务是解耦的,继承方式线程和任务是耦合的
    d.对于线程池来说,我们需要的是Runnable的实现类,而不需要Thread的子类
综上所述: 在开发中我们建议使用实现方式(并不是说继承方式不对)    

1.6 匿名内部类简化线程创建方式【重点】
匿名内部类作用:
	可以快速创建一个类的子类对象或者一个接口的实现类对象
public class TestDemo {
    public static void main(String[] args) {
        //1.继承方式创建线程
        new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 50; i++) {
                    System.out.println(Thread.currentThread().getName()+"..."+i);
                }
            }
        }.start();
        //2.实现方式创建线程
        new Thread(new Runnable(){
            @Override
            public void run() {
                for (int i = 0; i < 50; i++) {
                    System.out.println(Thread.currentThread().getName()+"..."+i);
                }
            }
        }).start();
        //主线程不会等待子线程任务结束
        for (int i = 0; i < 50; i++) {
            System.out.println(Thread.currentThread().getName()+"..."+i);
        }
    }
}        

第二章 高并发和线程安全【理解】

2.1 高并发及线程安全的介绍
什么是高并发: 是指在某个时间点上,有大量的用户(线程)同时访问同一资源
线程安全: 是指在某个时间点上,发生高并后,访问的数据出现"不合符实际的数据",称为线程安全有问题   

2.2 多线程的运行机制【内存方面】
public class MyThread extends Thread { 
    @Override 
    public void run() { 
        for (int i = 0; i < 100; i++) { 
            System.out.println("i = " + i); 
        } 
    } 
}

public class Demo { 
    public static void main(String[] args) { 
        //1.创建两个线程对象 
        MyThread t1 = new MyThread(); 
        MyThread t2 = new MyThread(); 
        //2.启动两个线程 
        t1.start(); 
        t2.start(); 
	} 
}

在这里插入图片描述

2.3 多线程安全性问题-可见性
什么有可见性:
	当一个共性变量,被多个线程使用时,其中某个线程对共性变量进行了修改,对于其他线程来说并不是立刻可见的
        其他线程获取的值还是以前的副本(旧的值)
案例:
	public class MyThread extends Thread {
        //无论创建多个MyThread对象,他们共性一个静态变量a
        public static int a = 0;
        @Override
        public void run() {
            System.out.println("线程启动,休息2秒...");
            try { Thread.sleep(1000 * 2); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println("将a的值改为1");
            a = 1;
            System.out.println("线程结束...");
        }
    }

	public class TestSafaDemo01 {
        public static void main(String[] args) {
            //1.启动线程
            MyThread t = new MyThread();
            t.start();
            //2.主线程继续
            while (true) {
                if (MyThread.a == 1) {
                    System.out.println("主线程读到了a = 1");
                }
            }
        }
    }


在这里插入图片描述

2.4 多线程的安全性问题-有序性
什么是有序性:
	在不影响代码的结果的程度上对代码进行"重排"
    如果在多线程的情况下,"重排"可能对一样的代码,执行后得出不一样的结果
    我们要保证在多线程的情况下,不对代码进行"重排",保证代码是有序(不要使用重排!!)    

在这里插入图片描述

2.5 多线程的安全性问题-原子性
什么原子性:
	线程对一个共性变量,进行++,这个++分成两步操作,先取出值加1 然后给共性变量赋值
    如果取出值加1,还没有来得及赋值,被其他线程抢走CPU,此时我们称为++操作不具有原子性  

在这里插入图片描述

第三章 volatile关键字【理解】

3.1 volatile是什么
volatile是一个关键字,用来修饰成员变量(静态变量),被他修饰的变量,具有可见性和有序性

3.2 volatile解决可见性
public class MyThread extends Thread {
    //无论创建多个MyThread对象,他们共性一个静态变量a
    public volatile static int a = 0;
    @Override
    public void run() {
        System.out.println("线程启动,休息2秒...");
        try { Thread.sleep(1000 * 2); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("将a的值改为1");
        a = 1;
        System.out.println("线程结束...");
    }
}

public class TestSafaDemo01 {
    public static void main(String[] args) {
        //1.启动线程
        MyThread t = new MyThread();
        t.start();
        //2.主线程继续
        while (true) {
            if (MyThread.a == 1) {
                System.out.println("主线程读到了a = 1");
            }
        }
    }
}

在这里插入图片描述

3.3 volatile解决有序性

在这里插入图片描述

3.4 volatile不能解决原子性
volatile不能解决原子性问题
public class MyThread extends Thread {
    public volatile static int a = 0;
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            a++;
        }
        System.out.println("修改完毕!");
    }
}

public class TestSafeDemo {
    public static void main(String[] args) throws InterruptedException {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();

        t1.start(); //线程1 对a加了10000次
        t2.start(); // 线程2 对a加了 10000次
        Thread.sleep(1000);
        System.out.println("获取a最终值:" + MyThread.a);
        //总是不准确的。原因:两个线程访问a 的步骤不具有:原子性
    }
}

小结:volatile的作用
a.解决变量的可见性,一旦变量发生改变,所有使用到该变量的线程都会取到最新值
b.解决变量的有序性,一旦变量加上volatile,那么编译器不会该变量的代码进行重排
c.无法解决变量操作过程中原子性,对变量的操作还是有可能被其他线程打断    

第四章 原子类【理解】

4.1 原子类【理解】
a.什么是原子类?
    是对普通类型(比如:int,Integer,double,Double)的原子类封装,使其的操作成员原子操作
b.原子类的作用?
    对原子类的增加或者减少操作,保证是原子性,保证中间不会被其他线程"打断"
c.原子类有哪些?
    比如:
		AtomicInteger是对int变量进行操作的原子类
        AtomicLong是对long变量进行操作的原子类
        AtomicBoolean对boolean变量操作的“原子类”;
注意: 原子类,既可以解决原子性,也可以解决有序性和可见性

4.2 AtomicInteger类示例【重点】
a.AtomicInteger是什么?
    是对int类型变量进行操作的原子类
b.AtomicInteger的构造方法
    public AtomicInteger(int num);
c.AtomicInteger的成员方法
    public int getAndIncrement();//就相当于 变量++  
	public int incrementAndGet();//就相当于 ++变量 
    
d.使用AtomicInteger改写案例    
    public class MyThread extends Thread {
        public static AtomicInteger a = new AtomicInteger(0);
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                a.getAndIncrement();//相当于 a++
            }
            System.out.println("修改完毕!");
        }
    }

	public class TestSafeDemo {
        public static void main(String[] args) throws InterruptedException {
            MyThread t1 = new MyThread();
            MyThread t2 = new MyThread();

            t1.start(); //线程1 对a加了10000次
            t2.start(); // 线程2 对a加了 10000次
            Thread.sleep(1000);
            System.out.println("获取a最终值:" + MyThread.a);
            //总是不准确的。原因:两个线程访问a 的步骤不具有:原子性
        }
    }
    

4.3 AtomicInteger类的工作原理-CAS机制【了解】

在这里插入图片描述

4.4 AtomicIntegerArray类示例【了解】
  • 非原子类数组在多线程并发时会有问题

    public class MyThread extends Thread {
        public static int[] intArray = new int[1000];//不直接使用数组
    
        @Override
        public void run() {
            for (int i = 0; i < intArray.length; i++) {
                intArray[i]++;
            }
        }
    }
    
    public class TestDemo01 {
        public static void main(String[] args) throws InterruptedException {
            //创建1000个线程,每个线程为数组的每个元素+1
            for (int i = 0; i < 1000; i++) {
                new MyThread().start();
            }
    
            Thread.sleep(1000 * 5);//让所有线程执行完毕
            System.out.println("主线程休息5秒醒来");
            for (int i = 0; i < MyThread.intArray.length; i++) {
                System.out.println(MyThread.intArray[i]);
            }
        }
    }
    打印结果:
    	1000,1000,1000,999,1000,1000,1000,1000,....
    有个别元素是小于1000,因为int[]是非原子类数组,不能保存原子性!!!
    
    
  • 使用原子类数组,保证原子性,解决问题

public class MyThread extends Thread {
    public static int[] intArray = new int[1000];//不直接使用数组
    public static AtomicIntegerArray arr = new AtomicIntegerArray(intArray);
    @Override
    public void run() {
//        for (int i = 0; i < intArray.length; i++) {
//            intArray[i]++;
//        }
        for (int i = 0; i < arr.length(); i++) {
            arr.addAndGet(i, 1);//将i位置上的元素 + 1,相当于 ++数组[i]
        }

    }
}

public class TestDemo01 {
    public static void main(String[] args) throws InterruptedException {
        //创建1000个线程,每个线程为数组的每个元素+1
        for (int i = 0; i < 1000; i++) {
            new MyThread().start();
        }

        Thread.sleep(1000 * 5);//让所有线程执行完毕
        System.out.println("主线程休息5秒醒来");
//        for (int i = 0; i < MyThread.intArray.length; i++) {
//            System.out.println(MyThread.intArray[i]);
//        }
        for (int i = 0; i < MyThread.arr.length(); i++) {
            System.out.println(MyThread.arr.get(i));
        }
    }
}

总结:
"说出进程和线程的概念
    进程: 正在内存中运行的程序
	线程: 进程中完成某个功能的执行单元
"能够理解并发与并行的区别
   并行: 两个线程真的真的真的一起运行  
   并发: 两个线程看起来一起运行,实际上交替执行    
能够描述Java中多线程运行原理[CPU]
   线程调度  
"能够使用继承类的方式创建多线程【重点】
     a.子类 继承 Thread
     b.子类 重写 run
     c.创建 子类 对象
     d.调用 子类 对象 start
       
"能够使用实现接口的方式创建多线程【重点】
     a.实现类 实现 Runnable
     b.实现类 重写 run
     c.创建 实现类 对象
     d.创建 Thread对象 同时 传入 实现类 对象
     e.调用 Thread对象 start 方法  
"使用匿名内部类快速创建 继承方式和实现方式的线程 【重点】     
       
能够说出实现接口方式的好处
能够解释安全问题的出现的原因
      可见性 有序性 原子性 
能够说出volatile关键字的作用
       解决 可见性 有序性 不能解决 原子性
"能够掌握原子类AtomicInteger的使用【重点】  
       创建:
	  AtomicInteger i = new AtomicInteger(10);
		使用:
		i.getAndIncrement(); //i++ 
		i.incrementAndGet(); //++i
能够理解原子类的工作机制
    因为原子类底层使用CAS机制(乐观锁机制,自旋机制)

发布了23 篇原创文章 · 获赞 0 · 访问量 1143

猜你喜欢

转载自blog.csdn.net/qq_44845814/article/details/104891339
今日推荐