复习
1.throw和throws
格式:
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
如果运行时异常,编译时不需要处理,运行后根据打印的异常信息,修改代码再次运行直到运行成功为止
今日内容
- 多线程
- volatile关键字
- 原子性
多线程
并行和并发
- 并行:指两个或多个事件在同一时刻发生(同时执行).
-
并发:指两个或多个事件在同一个时间段内发生(交替执行).
进程和线程
- 进程:正在内存中运行的程序
- 线程:进程中完成某个小功能的模块(进程中用执行某个功能的执行单元)
- 进程和线程的区别
- 线程是属于某个进程的.
- 进程:有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个线程。
- 线程:堆空间是共享的,栈空间是独立的(栈空间是线程向进程申请的独立空间),线程消耗的资源比进程小的多。
- 线程调度:(系统中所有的线程在某一纳秒时刻是在单独进行的. 线程调度是各个线程之间纳秒级切换进行)
- 分时调度:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
- 抢占式调度:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
- 主线程
当Java程序启动时,一个线程立刻运行,该线程通常叫做程序的主线程(main thread),因为它是程序开始时就执行的。主线程的重要性体现在两方面:
1.它是产生其他子线程的线程;
2.通常它必须最后完成执行,因为它执行各种关闭动作。
Thread类[重点]
- 介绍
运行一个java进程, 该进程中默认至少会有2个线程
1.main方法所在线程, 成为主线程
2.垃圾回收器线程
java中已经定义好了代表线程的类. 创建该类对象, 就是创建了一个线程.
线程开启我们需要用到了 java.lang.Thread 类, - Thread类的构造方法
public Thread() :分配一个新的线程对象。
public Thread(String name) :分配一个指定名字的新的线程对象。
public Thread(Runnable target) :分配一个带有指定目标新的线程对象。
public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字。
- Thread类的成员方法
public String getName() :获取当前线程名称。(线程有默认名字)
public void start() :导致此线程开始执行; Java虚拟机调用此线程的run方法。
public void run() :此线程要执行的任务在此处定义代码。
public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行,当前执行线程指,代码写在哪个线程中就指该线程)。
public static Thread currentThread() :返回对当前正在执行的线程对象的引用。
创建线程方式一_继承方式
- 将类声明为Thread的子类, 该子类重写Thread类的run方法, 接下来可以分配并启动该子类的实例.
- 创建具体步骤
1.定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把
run()方法称为线程执行体。
2.创建Thread子类的实例,即创建了线程对象
3.调用线程对象的start()方法来启动该线程 - 代码案例
public class MyThread extends Thread{
public MyThread(String name) {
super(name);
}
public MyThread() {
}
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println(getName()+i);
}
}
}
public class DemoThread {
public static void main(String[] args) {
MyThread myThread = new MyThread("我叫子线程");
myThread.start();
for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName()+i);
}
}
}
代码分析: 打印结果是 主线程和子线程交替出现, 是因为main方法为主线程, 和进行子线程时, 是并发进行的
在子线程中, 可以先获取当前对象,在调用该对象的getName()方法, 或者, 直接使用getName()方法也可以
Thread.currentThread().getname()
此方法通用在主线程或者子线程
创建线程的方式二_实现方式
- 采用 java.lang.Runnable接口 我们只需要重写run方法即可。
- 步骤如下:
1.定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体.
2.创建Runnable实现类的实例,此实现类的实例并不是线程对象.
3.需要以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。传入参数为Runnable接口的实现类
4.调用线程对象的start()方法来启动线程。 - 代码案例
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName()+i);
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
MyRunnable mr = new MyRunnable();
Thread tr = new Thread(mr,"我叫子线程");
tr.start();
for (int i = 0; i < 50; i++) {
// System.out.println("主线程"+i);
System.out.println(Thread.currentThread().getName()+i);
}
}
}
两种创建线程方式优劣比
实现Runnable接口比继承Thread类所具有的优势:
1.适合多个相同的程序代码的线程去共享同一个资源。(实现类方式, 线程和任务是分开的, 而直接继承的话,就直接把任务给定义到继承类中了, 给写死了)
2.可以避免java中的单继承的局限性。
3.增加程序的健壮性,实现解耦操作(继承方式线程和任务是耦合的),代码可以被多个线程共享,代码和线程独立。
4.线程池只能放入Runable实现类或Callable类线程,不能直接放入继承Thread的类。
匿名内部类简化线程创建方式[重点]
public class TestDemo {
public static void main(String[] args) {
new Thread(){
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName()+"------"+i);
}
}
}.start();
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);
}
}
}
高并发和线程安全
高并发及线程安全的介绍
- 高并发概念: 是指在某个时间点上,有大量的用户(线程)同时访问同一资源。
- 线程安全概念:在某个时间点上,当大量用户(线程)访问同一资源时,由于多线程运行机制的原因,可能会导致被访问的资源出现"数据污染"的问题。
多线程的运行机制[内存方面]
- 当一个线程启动后,JVM会为其分配一个独立的"线程栈区",这个线程会在这个独立的栈区中运行。
- 代码案例:
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();
}
}
- 图解进程中线程内存
- 多线程的安全性问题
-
多线程的安全性问题-可见性
共享变量,被多个线程使用时, 其中某个线程对共享变量进行修改或其他操作时, 此时其他线程对该操作未见, 拿到的值还是未操作之前的值.
可见性:是指线程之间的可见性,当一个线程修改一个变量,另外一个线程可以马上得到这个修改值。比如:用volatile修饰的变量是具有可见性的。volatile修饰的变量不允许线程内部缓存和
重新排序,即直接修改内存,所以对其它线程是可见的。但是这里需要注意一个问题volatile修饰的变量只是具有可见性,而不具备原子性。比如 volatile int a = 0; 之后操作一个 a++,那么a只是具有可见性,而不具备原子性,a++同样具有线程安全的问题. -
多线程的安全性问题- 有序性
有些时候“编译器”在编译代码时,会对代码进行“重排”
-
多线程的安全性问题-原子性
-
线程t1先读取a 的值为:0
t1被暂停
线程t2读取a的值为:0
t2将a = 1
t2将a写回主内存
t1将a = 1
t1将a写回主内存(将t2更改的1,又更改为1)
所以两次加1,但结果仍为1,少加了一次。
原因:两个线程访问同一个变量a的代码不具有"原子性". 原子性是指已经不能分隔的最小单位. (指一个操作可否完成)
原子性:在java语言原子性是指的一个不可以分割的操作,比如说 a = 0,这个就具有原子性,如果是a++, 这个操作其实是 a = a+1; 是可以分割的,所以他就不是一个原子性操作。
非原子操作具有线程安全问题,我们需要使用相关的手段,保证线程同步,
java的原子性和可见性区别
(1)原子性是拒绝多线程操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。例如 a=1是原子性操作,但是a++和a +=1就不是原子性操作。
(2)可见性volatile修饰词,可以应对多线程同时访问修改同一变量,由于相互的不可见性所带来的不可预期的结果。
volatile关键字
概念
- volatile是一个"变量修饰符",它只能修饰"成员变量",它能强制线程每次从主内存获取值,并能保证此变量不会被编译器优化。
- volatile能解决变量的可见性、有序性;
- volatile不能解决变量的原子性
volatile解决可见性
volatile解决有序性
当变量被修饰为volatile时,会禁止代码重排
volatile不能解决原子性
volatile小结
1.解决变量的可见性, 一旦变量发生改变, 所有使用该变量的线程都会取到最新值
2.解决变量的有序性, 编译时, 不会对有volatile修饰的成员变量进行重排
3.无法解决变量操作过程中的原子性
原子类
原子类概念
- 在java.util.concurrent.atomic包下定义了一些对“变量”操作的“原子类”:
原子类可以保证对“变量”操作的:原子性、有序性、可见性。
AtomicInteger类
- 概念
是对int类变量进行原子类封装操作 - AtomicInteger类构造方法
public AtomicInteger(int num);
- AtomicInteger类的成员方法
public int getAndIncrement();//相当于 变量++
public int incrementAndGet()//相当于 ++变量
AtomicInteger类的工作原理-CAS机制
AtomicIntegerArray类示例
public class MyThread extends Thread {
private static int[] intArray = new int[1000];//不直接使用数组
@Override
public void run() {
for (int i = 0; i < arr.length(); i++) {
intArray[i]++;
}
}
}
public class MyThread extends Thread {
private static int[] intArray = new int[1000];//定义一个数组
//改用原子类,使用数组构造
public static AtomicIntegerArray arr = new AtomicIntegerArray(intArray);
@Override
public void run() {
for (int i = 0; i < arr.length(); i++) {
arr.addAndGet(i, 1);//将i位置上的元素 + 1
}
}
}
public class Demo {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
new MyThread().start();
}
Thread.sleep(1000 * 5);//让所有线程执行完毕
System.out.println("主线程休息5秒醒来");
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机制(乐观锁机制,自旋机制)
网友评论