java多线程数据同步-java线程之间数据
同步和异步 Java 17 多线程
下面简单说一下这两个名词的概念。
同步同步
同步是指程序在运行过程中,必须等待处理结束,才能继续执行。 例如,main 函数包含三个方法:a()、b() 和 c()。
a调用bjava多线程数据同步,b调用c。 同步的概念是方法a执行完后,方法b执行,方法b执行完后,方法c执行。
对于每种方法,分别休眠 1 秒、2 秒和 3 秒。 在同步的情况下,执行时间为6秒。
演示代码如下:
import java.util.concurrent.TimeUnit;
public class Synchronize01 {
public static void main(String[] args) throws InterruptedException {
long beginTime = System.currentTimeMillis();
new Synchronize01().a();
long endTime = System.currentTimeMillis();
System.out.println("执行的总时间为:" + (endTime - beginTime) + " 毫秒");
}
public void a() throws InterruptedException{
TimeUnit.SECONDS.sleep(2);
b();
System.out.println("执行 a() 方法");
}
public void b() throws InterruptedException{
TimeUnit.SECONDS.sleep(1);
c();
System.out.println("执行 b() 方法");
}
public void c() throws InterruptedException{
TimeUnit.SECONDS.sleep(3);
System.out.println("执行 c() 方法");
}
}
完整代码及效果:
因为Java默认是同步执行的,下面的调用方法和上面的结果是一样的。
异步异步
异步意味着你不需要等待程序执行结束继续运行。 也就是执行了a()方法。 可以继续执行b()方法,也可以继续执行c()方法,前面两个方法没有执行完。
对于Java的异步的实现,需要封装成一个线程,让线程去执行。
同样是上面的a,b,c方法。
代码如下:
Thread ta = new Thread() {
@Override
public void run() {
try {
a();
long endTime = System.currentTimeMillis();
System.out.println("a() 执行的总时间为:" + (endTime - beginTime) + " 毫秒");
} catch (Exception e) {
}
}
}.start();
完整代码如下:
查看效果:
线程执行的结束时间作为结束时间,所以用的最多的就是总时间。 这样,程序的执行时间取决于线程中执行时间最长的方法。
在上面main方法的末尾添加两行代码。
long endTime = System.currentTimeMillis();
System.out.println("main() 执行的总时间为:" + (endTime - beginTime) + " 毫秒");
重新运行。
您发现 main 方法在 1 毫秒内执行完毕。
以上是异步和同步的简单概念。
在线程同步的程序中没有歧义。 不能同时操作的问题。
当异步方法执行时,可以发现其执行的规律性和随机性。 而且杂乱无章。
这时候你会遇到一个新的概念,就是多个线程同时访问一个变量。 导致并发时,数据与实际运行数据不一致。 通常称为“脏读”。 读取数据在读取过程中被修改。 之所以会出现这种情况,是因为多线程条件下读取实例变量存在线程安全问题。 每个人都在抢资源,不管是不是已经在别人手里了。 无法保证多线程实例变量或方法无法保证唯一性和原子性。
举一个存款和取款的例子。
首先创建两个线程来增加和减少数量。
代码如下:
public void add(Asynchronous02 asynchronous02) {
new Thread(() -> {
asynchronous02.money = asynchronous02.money.add(new BigDecimal("100"));
System.out.println("已存入,余额:" + asynchronous02.money);
}).start();
}
public void minus(Asynchronous02 asynchronous02) {
new Thread(() -> {
asynchronous02.money = asynchronous02.money.add(new BigDecimal("100").negate());
System.out.println("已取出,余额:" + asynchronous02.money);
}).start();
}
定义了一个变量:
private BigDecimal money = new BigDecimal("0");
完整代码如下:
查看运行效果:
为了方便测试脏读,将main方法中的代码修改如下:
try (Scanner scanner = new Scanner(System.in);) {
System.out.println("菜单");
System.out.println("1. 存入金额 100");
System.out.println("2. 取出金额 100");
System.out.println("3. 查询当前余额");
System.out.println("q. 退出");
Asynchronous02 asynchronous02 = new Asynchronous02();
String line;
while (scanner.hasNextLine() && (line = scanner.nextLine()) != null) {
System.out.println("=================");
switch (line) {
case "1":
for (int i = 0; i < 40; i++) {
asynchronous02.add(asynchronous02);
}
break;
case "2":
asynchronous02.minus(asynchronous02);
break;
case "3":
System.out.println("当前余额:" + asynchronous02.money);
break;
default:
System.out.println("退出系统成功");
System.exit(0);
}
}
} catch (Exception e) {
}
add方法执行时,直接启动40个线程进行调用。 运行看看效果:
应该发现余额应该是4000,但是才3900,这肯定是不能接受的。 这样的血汗钱竟然吞了我100块钱。
需要注意的是,以上演示纯粹是为了演示目的。
同步关键字
这时候synchronized关键字就登场了。 同步语句的意思是当线程执行时,获取互斥量,然后执行block的内容,执行完成后释放锁。 如果执行线程已经加锁,其他线程就不能再获取锁。
同步块
修改我们的程序java多线程数据同步,调整线程中run方法的内容如下:
public void add(asynchronous03 asynchronous03) {
new Thread(() -> {
synchronized(this){
asynchronous03.money = asynchronous03.money.add(new BigDecimal("100"));
System.out.println(Thread.currentThread().getName() + "已存入,余额:" + asynchronous03.money);
}
}).start();
}
为了便于查看,添加了线程名称。
完整代码如下图所示:
这样,重新运行代码。 运行后查看效果:
你可以看到结果。 虽然线程乱了,但是每次访问都会等待上一个线程释放线程锁。 所以加量是有规律的。 这样就达到了我们的目的。 保证数据的原子性和一致性。 确保业务的正确运行。
您也可以在此处对方法进行同步。 因为这个方法中使用了线程,如果写到方法中,线程安全就得不到保证。 所以不能用this关键字来写方法的用法,只能用synchronized代码块。
语句块如何保证其锁定状态。 写一段简单的代码。 为了方便我们查看字节码的内容。
使用javap -c -v Asynchronous04.class 查看字节码内容。
找到添加方法。
可以看到monitorenter和monitorexit命令。 在Java 17虚拟机中,该方法类用于控制同步的进入和退出。 它称为同步构造或监视器。 这只是语句块的方式。 如果是method,则不使用这种方式,而是使用运行时常量池中的ACC_SYNCHRONIZED标志来区分。
同步方法修饰
修改上面的代码。
import java.math.BigDecimal;
public class Asynchronous05 {
private BigDecimal money = new BigDecimal("0");
public static void main(String[] args) throws InterruptedException {
Asynchronous05 asynchronous05 = new Asynchronous05();
asynchronous05.add();
}
public synchronized void add() {
money = money.add(new BigDecimal("100"));
System.out.println(Thread.currentThread().getName() + " 已存入,余额:" + money);
}
}
再次使用javap -c -v Asynchronous05.class 查看字节码内容的变化。
在字节码中找到add方法。 查看字节码常量池的flags内容。
可以看到常量池中的代码
public synchronized void add();
descriptor: ()V
flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
从心智上来说,如果要实现同步,就必须告诉虚拟机我要同步。 虚拟机说好,我来当你的看守,等你走的时候告诉我,我就放过下一个。 你不出来,守卫就在那里等着你出来。
同步和异步的简单使用,异步中如何保证数据的唯一性。 关于线程的知识还是很多的。 今天就到此为止。
谢谢阅读。 欢迎关注、收藏、点赞。 更多内容会持续更新。