多线程并发环境下,数据的安全问题(重点)
存在安全问题的三个条件:
1.多线程并发
2.有共享数据
3.共享数据有修改的行为
满足以上3个条件之后,就会存在线程安全问题。
解决方法:线程同步机制,让线程排队执行。
线程同步机制的语法是:
synchronized (){
// 线程同步代码块
}
synchronized () 括号中传的数据必须是多线程共享的数据。
synchronized
出现在实例方法
上,一定锁的是this。
public synchronized void 方法名(){}
缺点:这种方式表示整个方法体都需要同步,可能会无故扩大同步的范围,导致程序的执行效率降低。所以这种方式不常用。
优点:代码简洁。如果共享的对象就是this,并且需要同步的代码块是整个方法体,建议使用这种方式。
synchronized
出现在静态方法
上找的是类锁(类锁只有1把)。
public synchronized static void 方法名(){}
对象锁:1个对象1把锁,100个对象100把锁。
类锁:100个对象,也可能只是1把类锁。
Java中有三大变量?【重要的内容。】
以上三大变量中:
局部变量
永远都不会存在线程安全问题。局部变量不共享。
堆
和方法区
都是多线程共享的,所以可能存在线程安全问题。
局部变量、常量:不会有线程安全问题。
成员变量:可能会有线程安全问题。
如果使用局部变量的话,建议使用:StringBuilder(线程不安全,效率高)。因为局部变量不存在线程安全问题。
synchronized在开发中不建议嵌套使用,一不小心就很容易出现死锁现象。
死锁很难调试,并且不会报错。
public class DeadLock {
public static void main(String[] args) {
Object o1 = new Object();
Object o2 = new Object();
// t1和t2两个线程共享o1,o2
MyThread1 t1 = new MyThread1(o1, o2);
MyThread2 t2 = new MyThread2(o1, o2);
t1.start();
t2.start();
}
}
class MyThread1 extends Thread {
Object o1;
Object o2;
public MyThread1(Object o1, Object o2) {
this.o1 = o1;
this.o2 = o2;
}
public void run() {
synchronized (o1){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2){
System.out.println("MyThread1");
}
}
}
}
class MyThread2 extends Thread {
Object o1;
Object o2;
public MyThread2(Object o1, Object o2) {
this.o1 = o1;
this.o2 = o2;
}
public void run() {
synchronized (o2){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){
System.out.println("MyThread2");
}
}
}
}
以上代码永远不会输出,且一直在运行中,不会结束。
java语言中线程分为两大类:
守护线程的特点:
一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束。
注意:主线程main方法是一个用户线程。
public class ThreadTest11 {
public static void main(String[] args) {
BakDataThread t1 = new BakDataThread();
t1.setName("t1");
// 启动线程之前,将线程设置为守护线程
t1.setDaemon(true);
t1.start();
// 主线程,是用户线程
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "-->" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class BakDataThread extends Thread {
public void run() {
int i = 0;
// 死循环
// 由于该线程是守护者,当用户线程结束,守护线程自动终止(即死循环结束)。
while (true) {
System.out.println(Thread.currentThread().getName() + "-->" + (++i));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
守护线程一般使用在每天00:00的时候系统数据自动备份。
这个需要使用到定时器,并且我们可以将定时器设置为守护线程。每到00:00的时候就备份一次。所有的用户线程结束了,守护线程自动退出,没有必要进行数据备份了。
作用:间隔特定的时间,执行特定的程序。
定时器实现方式:
可以使用sleep方法,睡眠,设置睡眠时间,到这个时间点醒来,执行任务。这种方式是最原始的定时器。(比较low)
在java的类库中已经写好了一个定时器:java.util.Timer,可以直接拿来用。(目前很少使用)
在实际的开发中,目前使用较多的是Spring框架中提供的SpringTask框架,这个框架只要进行简单的配置,就可以完成定时器的任务。
/*
使用定时器指定定时任务。
方法:
void schedule(TimerTask task, Date firstTime, long period)
安排指定的任务在指定的时间开始进行重复的固定延迟执行。
*/
public class TimerTest {
public static void main(String[] args) throws Exception {
// 创建定时器对象
Timer timer = new Timer();
//Timer timer = new Timer(true); // 守护线程方式
// 指定定时任务
//timer.schedule(定时任务,第一次执行时间,间隔多久执行一次);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date fistTime = sdf.parse("2022-04-18 11:15:00");// 字符串转换成日期
timer.schedule(new LogTimerTask(),fistTime,1000*10);
}
}
// 编写一个记录日志的定时任务类
class LogTimerTask extends TimerTask {
@Override
public void run() {
// 需要执行的任务
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String strTime = sdf.format(new Date());
System.out.println(strTime + "成功完成一次数据备份");
}
}
/*
实现线程的第三种方式:实现Callable接口
优点:可以获取到线程的执行结果
缺点:效率比较低
*/
public class ThreadTest12 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 第一步:创建一个“未来任务类”对象
FutureTask task = new FutureTask(new Callable() {
// 相当于run方法,只不过call方法有返回值
@Override
public Object call() throws Exception {
// 模拟执行
System.out.println("call method begin");
Thread.sleep(1000*3);
System.out.println("call method end");
int a = 100;
int b = 200;
return a + b; //自动装箱(变成Integer类型)
}
});
// 创建线程对象
Thread t = new Thread(task);
// 启动线程
t.start();
// 在主线程获取t线程的执行结果
// get()方法的执行会导致当前线程阻塞
Object obj = task.get();
System.out.println("线程执行结果:" + obj);
// main方法这里的程序想要执行必须等待get()方法结束
System.out.println("get end");
}
}
wait和notify方法不是线程对象的方法,是java中任何一个java对象都有的方法,因为这两个方式是Object类中自带的。
wait方法和notify方法的使用建立在synchronized线程同步的基础之上。
wait()方法
Object o = new Object();
o.wait();
作用:让正在o对象上活动的当前线程进入等待状态,无期限等待,直到被唤醒为止。(会释放之前占有的o对象的锁)
notify()方法
Object o = new Object();
o.notify();
作用:唤醒正在o对象上等待的线程。(只会通知,不会释放之前占有的o对象的锁)
还有一个notifyAll()方法:
这个方法是唤醒o对象上处于等待的所有线程。
/*
使用wait和notify方法实现”生产者和消费者模式“
生产一个消费一个(死循环,程序需要手动停止)
*/
public class ThreadTest {
public static void main(String[] args) {
// 创建一个仓库对象,共享的
List list = new ArrayList();
// 创建两个线程对象
// 生产者线程
Thread t1 = new Thread(new Producer(list));
// 消费者线程
Thread t2 = new Thread(new Consumer(list));
t1.setName("生产者线程");
t2.setName("消费者线程");
t1.start();
t2.start();
}
}
// 生产线程
class Producer implements Runnable {
// 仓库
private List list;
public Producer(List list) {
this.list = list;
}
@Override
public void run() {
// 一直生产
while (true) {
// 给仓库对象list加锁
synchronized (list){
if (list.size() > 0) { // >0 表示线程中有一个元素了
// 当前线程进入等待状态,并且释放Producer之前占有的list集合的锁。
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 仓库为空,进行生产
Object obj = new Object();
list.add(obj);
System.out.println(Thread.currentThread().getName() + "-->" + obj);
// 唤醒消费者进行消费
list.notify();
}
}
}
}
// 消费线程
class Consumer implements Runnable {
// 仓库
private List list;
public Consumer(List list) {
this.list = list;
}
@Override
public void run() {
// 一直消费
while (true) {
synchronized (list) {
if (list.size() == 0) { // 表示仓库已经空了
try {
// 消费者线程等待,释放掉list集合的锁
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 仓库有数据,进行消费
Object obj = list.remove(0);
System.out.println(Thread.currentThread().getName() + "-->" + obj);
// 唤醒消费者进行消费
list.notify();
}
}
}
}
/*
要求输出:
t1-->1
t2-->2
t1-->3
t2-->4
t1-->5
t2-->6
...
*/
public class HomeWork {
public static void main(String[] args) {
// 创建一个Num对象
Num num = new Num(1);
// 创建两个线程对象
Thread t1 = new Thread(new T1(num));
Thread t2 = new Thread(new T2(num));
t1.setName("t1");
t2.setName("t2");
t1.start();
t2.start();
}
}
class Num{
int i;
public Num(int i) {
this.i = i;
}
}
// t1
class T1 implements Runnable {
// 仓库
private Num num;
public T1(Num num) {
this.num = num;
}
@Override
public void run() {
while (true) {
synchronized (num) {
if (num.i%2 == 0) {
try {
num.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "-->" + num.i++);
// 唤醒t2
num.notify();
}
}
}
}
// t2
class T2 implements Runnable {
// 仓库
private Num num;
public T2(Num num) {
this.num = num;
}
@Override
public void run() {
while (true) {
synchronized (num) {
if (num.i%2 == 1) {
try {
num.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "-->" + num.i++);
// 唤醒t1
num.notify();
}
}
}
}