71.线程安全
即多线程并发环境下,线程的安全问题;
多线程并发对同一个账号取款;
触发线程安全问题需要的3个条件:
①存在多线程并发;
②共享数据;
③对共享数据进行修改;
只要满足以上3个条件之后,就会存在线程安全问题;
可以用线程排队(不能并发)解决线程安全问题,这种机制被称为线程同步机制;
线程同步(线程排队)就会牺牲一部分效率,但是效率的前提是数据安全,没有数据安全,效率无从谈起;
线程之间各自独立执行,不需要等待其他线程,这种编程模型叫做异步编程模型,也叫多线程并发,效率较高;
线程排队执行,线程之间发生了等待关系,这就是同步编程模型,效率较低;
多线程并发及其解决Demo:https://blog.csdn.net/y_w_x_k/article/details/124233368
JAVA三大变量:
局部变量 在栈中
实例变量 在堆中
静态变量 在静态方法区
局部变量永远是线程安全的,因为局部变量在栈中,一个线程一个栈,线程之间不存在共享一个栈,局部变量永远不共享;
实例变量在堆中,堆只有一个;
静态变量在静态方法区中,静态方法区只有一个;
堆和方法区都是多线程共享的,所以可能存在线程安全问题;
常量也不存在线程安全问题,因为常量不可修改;
字符串操作中,如果是局部变量的话,建议使用StringBuilder,因为局部变量不存在线程安全问题,用StringBuffer每次需要经过线程池,执行变慢了;
ArrayList是非线程安全的;
Vector是线程安全的;
HashMap,HashSet是非线程安全的;
HashTable是线程安全的;
synchronized的三种写法:
①同步代码块
synchronized (this) {
double bal_before = this.getBalance();
double bal_after = bal_before - money;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
this.setBalance(bal_after);
}
②用synchronized修饰方法体
public synchronized void moneyOut(double money) {
...
}
③在静态方法上使用synchronized
表示找类锁,类锁永远只有1把,就算创建了100个对象,类锁也只有1把;
对象锁:100个对象100把锁;
类锁:100个对象,也可能只有1把类锁;
类锁详细demo:https://blog.csdn.net/y_w_x_k/article/details/124233368
72.死锁
程序出现死锁将会不出现异常,也不会出现错误,程序一直僵持在那里,这种错误最难调试;
public class ThreadRunable implements Runnable {
Object o1;
Object o2;
public ThreadRunable(Object o1, Object o2) {
this.o1 = o1;
this.o2 = o2;
}
@Override
public void run() {
// TODO Auto-generated method stub
synchronized (o1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized (o2) {
System.out.println("线程1运行。。。");
}
}
}
}
public class ThreadRunable2 implements Runnable {
Object o1;
Object o2;
public ThreadRunable2(Object o1, Object o2) {
this.o1 = o1;
this.o2 = o2;
}
@Override
public void run() {
// TODO Auto-generated method stub
synchronized (o2) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized (o1) {
System.out.println("线程2运行。。。");
}
}
}
}
public static void test30() {
Object o1 = new Object();
Object o2 = new Object();
Thread t1 = new Thread(new ThreadRunable(o1, o2));
Thread t2 = new Thread(new ThreadRunable2(o1, o2));
t1.start();
t2.start();
}
在开发中最好不要将synchronized嵌套使用,容易发生死锁,不容易排查错误;
73.开发中synchronized的使用
synchronized会让程序的执行效率降低,用户体验不好,系统的用户吞吐量降低,再实在没有更好的解决方案的情况下才考虑用线程同步;
开发者中避免使用synchronized的方案:
第一种方案:尽量使用局部变量来代替实例变量和静态变量;
第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了(一个线程对应一个对象),对象不共享,就没有数据安全问题了;
如果不能使用局部变量,也不能创建多个对象,那只能使用synchronized了,使用线程同步机制;
74.守护线程
JAVA语言中,线程分为两大类:
一类是:用户线程;
一类是:守护线程(后台线程)
其中最有代表性的就是垃圾回收线程(守护线程);
守护线程的特点:
守护线程一般是一个死循环,只要用户线程全部结束,守护线程也会自动结束;
主线程main方法是一个用户线程;
public class DefendThread implements Runnable {
@Override
public void run() {
// TODO Auto-generated method stub
for (int i = 0; i < 100; i++) {
System.out.println("守护线程i--" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
public static void test31() {
Thread t = new Thread(new DefendThread());
// 设置守护线程,主线程结束,守护线程也自动结束
t.setDaemon(true);
t.start();
for (int i = 0; i < 5; i++) {
System.out.println("主线程i--" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
75.定时器
java.util.timer定时器,可以实现定时器功能,但在实际开发中也很少用,因为很多高级框架都是支持定时任务的;
实际开发中,目前使用较多的是Spring框架中提供的SpringTask框架,这个框架只要通过简单的配置就能完成定时任务;
public class TimerTest extends TimerTask {
@Override
public void run() {
// 编写定时任务
// TODO Auto-generated method stub
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(sdf.format(new Date()) + ":备份数据");
}
}
public static void test32() throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date firstDate = sdf.parse("2022-04-18 12:51:00");
Timer t = new Timer();
//参数:定时任务,第一次执行时间,执行间隔
t.schedule(new TimerTest(), firstDate, 5 * 1000);
}
//匿名内部类方式
public static void test32() throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date firstDate = sdf.parse("2022-04-18 12:51:00");
Timer t = new Timer();
t.schedule(new TimerTask() {
@Override
public void run() {
// TODO Auto-generated method stub
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(sdf.format(new Date()) + ":备份数据");
}
}, firstDate, 5 * 1000);
}
76.JDK8实现线程新方式--Callable
这个方式实现的线程可以获取返回值;
继承Thread与实现Runale接口方式是没法获取线程返回值的,因为run方法返回void;
委派一个线程去执行一个任务,该任务执行完后可能会有一个执行结果,Callable实现线程可以获取这个执行结果;
import java.util.concurrent.FutureTask; //JUC下的,属于java的并发包,老JDK下没有这个包,新特性
public static void test33() throws Exception {
FutureTask ft = new FutureTask<>(new Callable<>() {
@Override
public Object call() throws Exception {
Thread.sleep(2000);
// TODO Auto-generated method stub
return 300;
}
});
Thread t = new Thread(ft);
t.start();
// 这里是main方法,主线程中
// 获取t线程的返回结果
// get方法执行会导致当前线程阻塞
Object o = ft.get();
System.out.println(o);
/**
* main方法想要继续执行必须等待get方法的结果
* get方法可能要很久,因为get方法是为了拿到另一个线程的执行结果
* 另一个线程执行需要时间
*/
System.out.println("主线程下一步...");
}
这种方式的优点是可以得到线程的返回结果;
缺点是效率比较低,在获取t线程的执行结果的时候,会阻塞当前线程;
77.Object的wait和notify方法(生产者和消费者模式)
wait和notify方法不是线程对象的方法,是java中任何一个java对象都有的方法,因为这两个方法是Object类自带的;
wait方法的作用:
Object o=new Object();
o.wait();
让正在o对象上活动的t线程进入无限期等待状态,并且释放掉t线程之前占有的o对象的锁,直到被唤醒为止;
o.wait()的调用会让正在o对象上活动的当前线程进入等待状态;
notify方法的作用:
Object o=new Object();
o.notify();
唤醒正在o对象上等待的当前线程(当前线程),只是通知,不会释放o对象上之前占有的锁;
notifyAll:
Object o=new Object();
o.notify();
唤醒o对象上等待的所有线程;
生产者和消费者模式是为了专门解决某个特定的需求的;
生产者和消费者模式:
生产线程负责生产,消费线程负责消费,生产线程和消费线程需要达到均衡,这是一种特殊的业务需求,在这种特殊情况下需要使用wait和notify方法;
wait方法和notify方法建立在线程同步的基础之上,因为多线程要同时操作一个仓库,有线程安全问题;
Demo:
/**
* 模拟一个场景
* List集合中假设只能存储一个元素
* 一个元素就表示仓库满了
* 如果List集合中的元素个数是0,就表示仓库空了
* 保证List集合中一直是最多存储一个元素
* 生产一个消费一个
*/
public class ThreadTestWN {
public static void doTest() {
List list = new ArrayList();
Thread t1 = new Thread(new Producer(list));
Thread t2 = new Thread(new Customer(list));
t1.setName("生产者线程");
t2.setName("消费者线程");
t1.start();
t2.start();
}
}
// 生产者线程
class Producer implements Runnable {
List list;
public Producer(List list) {
this.list = list;
}
@Override
public void run() {
// TODO Auto-generated method stub
while (true) {
synchronized (list) {
// 给仓库对象list加锁
if (list.size() > 0) {
try {
// 已经生产完毕,当前线程进入等待状态,并且释放Producer之前占有的list集合的锁
list.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
// 程序执行到这说明集合没有元素,进行生产动作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
Object o = new Object();
this.list.add(o);
// 生产完毕唤醒其他线程,此时生产者没有释放锁,程序继续执行while循环,此时已经生产完毕,if判断为true,执行wait方法,生产者进入等待状态;
System.out.println(Thread.currentThread().getName() + "生产---->" + o);
list.notify();
}
}
}
}
// 消费者线程
class Customer implements Runnable {
List list;
public Customer(List list) {
this.list = list;
}
@Override
public void run() {
// TODO Auto-generated method stub
while (true) {
synchronized (list) {
if (list.size() == 0) {
// 已经消费完毕,当前线程线程等待,释放Customer占有的list锁
try {
list.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
// 程序执行到这里说明集合有元素,进行消费动作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
Object o = list.remove(0);
// 消费完毕,此时List集合没有元素,唤醒o对象所有占用当前线程,此时消费者没有释放锁,程序会继续执行while循环,之后进入if判断,为true,再执行wait方法,消费者进入等待状态
System.out.println(Thread.currentThread().getName() + "消费---->" + o);
list.notify();
}
}
}
}
78.反射机制
通过java语言中的反射机制可以操作字节码文件(可以读和修改字节码文件,class文件);
在java.lang.reflect包下;
反射机制相关的重要的类:
java.lang.class:代表字节码文件,代表一个类型;
java.lang.Reflect.Method:代表字节码中的方法字节码;
java.lang.Reflect.Constructor:代表字节码中的构造方法字节码;
java.lang.Reflect.Field:代表字节码中的属性字节码;
要操作一个类的字节码,需要首先获取到这个类的字节码,有3中方式获取这个类的字节码:
方式一:Class c=Class.forName("java.lang.String"),括号内是完成类名带包名
方式二: Class c=引用.getClass();getClass是Object类的方法,返回此 Object
的运行时类
方式三:Class c=类名.class;JAVA语言中任何一种类型,包括基本数据类型,都有class属性
public static void test35() {
//forName方式
/**
* 静态方法
* 方法在参数是一个字符串
* 字符串需要的是一个完整类名
* 完整类名必须带有包名,java.lang包也不能省略
*/
Class c1 = null;
Class c4 = null;
try {
c1 = Class.forName("java.lang.String"); // c1代表String.class文件,或者说c1代表String类型
Class c2 = Class.forName("java.lang.Integer"); // c2代表Integer.class文件,或者说c1代表Integer类型
Class c3 = Class.forName("java.lang.System"); // c3代表System.class文件,或者说c1代表System类型
c4 = Class.forName("java.util.Date"); // c4代表Date.class文件,或者说c1代表Date类型
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//getClass方式
String s = "accc1";
Class s1 = s.getClass();
System.out.println(s1 == c1); // true
Date d = new Date();
Class d1 = d.getClass();
System.out.println(d1 == c4); // true
Class s2 = String.class;
Class d2 = Date.class;
System.out.println(s2 == s1); // true
System.out.println(d2 == d1); // true
}
后期需要学习的高级框架,例如Spring...都用到了反射机制,所以反射机制还是重要的;学习反射机制有利于理解剖析框架底层的源代码;
Class.forName原理:
public class Movie {
public Movie() {
System.out.println("movie无参构造");
}
// 静态代码块,类加载的时候执行,并且只执行一次
//如果只希望一个类的静态代码块执行,其他代码不执行,可以使用Class.forName("完整类名")
//Class.forName这个方法的执行会导致类加载
//类加载后,静态代码块执行
static {
System.out.println("Movie静态代码块");
}
}
public static void test38() {
try {
Class.forName("ReflectTest.Movie");
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
79.反射机制创建对象
public static void test36() {
try {
Class mc = Class.forName("ReflectTest.Movie");
// newInstance方法会调用Movie类的无参构造方法,完成对象的创建,必须保证Movie类的无参构造是存在的
Object m = mc.newInstance();
System.out.println(m);
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InstantiationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
反射机制的好处
// java代码写一遍,在不改变源代码的基础上,可以做到不同对象的实例化,非常灵活
// 符合OCP开闭原则,对扩展开放,对修改关闭
public static void test37() throws Exception {
// 这个方法是写死的,不够灵活
// Movie m = new Movie();
// 以下代码是灵活的,修改配置文件,可以创建出不同的实例对象
FileReader fr = new FileReader("C:\\Users\\Administrator\\Desktop\\临时\\aa.txt");
Properties pro = new Properties();
pro.load(fr);
//或者直接以流的形式返回
//pro.load(Thread.currentThread().getContextClassLoader().getResourceAsStream("ReflectTest/aa.txt"));
String className = pro.getProperty("className");
Class cls = Class.forName(className);
Object obj = cls.newInstance();
System.out.println(obj);
fr.close();
}
80.通用方式获取路径
使用此方式获取路径即使代码位置变更仍然能获取路径;
通用方式的前提是文件在类路径下;
凡是在src文件夹下的文件都是类路径下,src是类的根路径;
public static void test39() {
/**
* currentThread()获取当前线程
* getContextClassLoader():线程对象的方法,获取当前线程的类加载器对象
* getResource():获取资源,这是类加载器对象的方法,当前线程的类加载器对象默认从类的根路径下加载资源,从src目录作为起点开始
* 这种方式获取绝对路径是通用的
*/
String path = Thread.currentThread().getContextClassLoader().getResource("ReflectTest/aa.txt").getPath();
// 采用以上的代码可以拿到一个文件的绝对路径
System.out.println(path);
}