当前位置: 首页 > news >正文

并发编程(4)

final修饰

1. 用final修饰类

当一个类被final修饰时,意味着它不能被其他类继承,也就是该类无法派生出子类。像 Java 中的String类就是典型的final类。

public final class FinalClass {// 类的内容
}// 下面的代码会报错,因为FinalClass不能被继承
// public class SubClass extends FinalClass {} 

2. 用final修饰方法

若一个方法被final修饰,那么这个方法不能被子类重写(但可以被继承和调用)

public class Parent {public final void display() {System.out.println("这是一个final方法");}
}public class Child extends Parent {// 下面的代码会报错,因为无法重写final方法// @Override// public void display() {} 
}

3. 用final修饰变量

(1)修饰基本数据类型变量

一旦基本数据类型的变量被final修饰,它的值就不能再被改变

final int num = 10;
// num = 20;  // 这行代码会报错,因为不能对final变量重新赋值

(2)修饰引用类型变量

当引用类型的变量被final修饰时该变量不能再指向其他对象但对象自身的内容是可以修改的。

final List<String> list = new ArrayList<>();
list.add("apple");  // 允许修改对象的内容
// list = new ArrayList<>();  // 这行代码会报错,因为不能让final变量指向新对象

实例代码:

package bf;public class Test {public static void main(String[] args) throws Exception {Test1 x1 = new Test1();Thread t1 = new Thread() {@Overridepublic void run() {for (int i = 0; i < 100000; i++) {x1.flag++;}}};Thread t2 = new Thread() {@Overridepublic void run() {for (int i = 0; i < 100000; i++) {x1.flag++;}}};t1.start();//进入就绪态t2.start();//进入就绪态t1.join();t2.join();System.out.println(x1.flag);}
}

对于子线程来说,主线程的变量都是加了final。对于x1引用对象(类),我们不能改变他的指向,但可以改变他的值。(子线程想要改变主线程的变脸就要用引用类型)

4. 用final修饰参数

如果方法参数被final修饰,那么在方法内部不能对该参数进行重新赋值。

public void print(final String text) {// text = "new text";  // 这行代码会报错,因为不能给final参数重新赋值System.out.println(text);
}

finalvolatile在防止指令重排序上有相似之处

防止重排序 :二者都能在一定程度上防止指令重排序。final通过禁止特定的指令重排序来保证对象的安全发布;volatile修饰的变量,编译器和处理器会插入内存屏障,防止对volatile变量相关操作的重排序 ,保障多线程环境下操作的有序性。

实例代码:

package bf;public class Test {public static void main(String[] args) throws Exception {Test1 x1 = new Test1();Thread t1 = new Thread() {@Overridepublic void run() {for (int i = 0; i < 100000; i++) {x1.flag++;}}};Thread t2 = new Thread() {@Overridepublic void run() {for (int i = 0; i < 100000; i++) {x1.flag++;}}};t1.start();//进入就绪态t2.start();//进入就绪态t1.join();t2.join();System.out.println(x1.flag);}
}
package bf;public class Test1 {public volatile int flag;}
结果

答案不是20000,所以说volatile不能保证多线程下的数据安全问题,

数据安全问题:多线程对同一变量进行操作,最终结果和预想不一样。

 volatile只保证读正确,不保证写正确。在子线程读的时候,所有子线程读都是对的,但往回写会互相覆盖。

volatilesynchronized

volatilesynchronized是两个不同的同步机制,它们的使用范围和作用确实有明显区别:

1. volatile 的使用限制

  • 只能修饰变量
    volatile 用于修饰类的成员变量(实例变量或静态变量),不能修饰方法、参数或局部变量。
    public class Example {private volatile int count; // 合法:修饰实例变量private static volatile boolean flag; // 合法:修饰静态变量
    }
    
  • 不能修饰方法
    如果尝试用 volatile 修饰方法,会导致编译错误。
    public volatile void doSomething() { // 错误:不能修饰方法// ...
    }
    

2. synchronized 的使用限制

  • 可以修饰方法
    synchronized 可以修饰实例方法、静态方法,也可以用于代码块
    public class Example {public synchronized void instanceMethod() { // 合法:修饰实例方法// ...}public static synchronized void staticMethod() { // 合法:修饰静态方法// ...}public void someMethod() {synchronized (this) { // 合法:同步代码块// ...}}
    }
    
  • 不能直接修饰变量
    synchronized 不能直接用于修饰变量,只能通过同步方法或代码块间接保护变量的访问。
    private synchronized int count; // 错误:不能修饰变量
    

写后读

在并发编程中,写后读(Write-Read) 是一种关键的内存操作顺序,指一个线程写入变量后(增删改,内存的更新),另一个线程读取该变量(读取内存数据)。正确处理写后读操作对于保证多线程程序的正确性至关重要。

下面的列子,两个线程先读到x=0,然后又都付了值。要想实现数值准确,要实现基于其他线程的计算结果上继续计算。也就是说,当第一个x=10写回到内存后,x=6要累加到内存的x=10上。

实例代码

package demo;public class Test1 {public volatile int flag;public synchronized void add() {flag++;}
}package demo;public class Test {public static void main(String[] args) throws Exception {Test1 x1 = new Test1();Thread t1 = new Thread() {@Overridepublic void run() {for (int i = 0; i < 100000; i++) {x1.add();}}};Thread t2 = new Thread() {@Overridepublic void run() {for (int i = 0; i < 100000; i++) {x1.add();}}};t1.start();t2.start();t1.join();t2.join();System.out.println(x1.flag);}
}

答案

再说一下synchronized锁,上面的代码两个线程栈要调用add方法对flag++。但add已经加锁了,意味着两个线程只有竞争出一个来使用这个方法,比如说1线程先使用add方法,但还没使用完时间片就到期了,2线程被分配了时间片,但在他调用add时发现不让他读也不能拷贝。所以通过synchronized可以实现写后读。

再换一种写法:

package bf;
public class Test {public static void main(String[] args) throws Exception {Test1 x1 = new Test1();Thread t1 = new Thread() {@Overridepublic void run() {for (int i = 0; i < 100000; i++) {int w=x1.get()+1;x1.set(w);}}};Thread t2 = new Thread() {@Overridepublic void run() {for (int i = 0; i < 100000; i++) {int w=x1.get()+1;x1.set(w);}}};t1.start();t2.start();t1.join();t2.join();System.out.println(x1.flag);}
}package bf;public class Test1 {public volatile int flag;public synchronized void add() {flag++;}public synchronized int get() {return flag;}public synchronized void set(int x) {flag = x;}}

结果

 add() set() get()三个方法都使用了synchronized锁。都属于加锁的静态方法,都是对象锁。调用某个方法时会锁住整个对象,(其他线程想要调用这三个也不允许)锁的是整个x1对象。

这个代码流程是这样的,线程1先调用get()方法读了flag并且上锁,执行完了后没有进行写操作就释放锁了。若此时线程1的时间片到期了,线程2获得权限就会造成线程1的flag还没加回内存。

多线程。多服务器。。只要是写后读结果都是正确的(与语言无关)

实例2

Shop.java

package bf;public class Shop {public synchronized void m1() {System.out.println("m1开始");try {Thread.sleep(5000);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}System.out.println("m1结束");}public synchronized void m2() {System.out.println("m2开始");System.out.println("m2结束");}public static synchronized void m3() {System.out.println("m3开始");System.out.println("m3结束");}public static synchronized void m4() {System.out.println("m4开始");System.out.println("m4结束");}public void m5() {System.out.println("m5开始");System.out.println("m5结束");}
}

Test.java

package bf;public class Test {public static void main(String[] args) throws Exception {Shop x1 = new Shop();Shop x2 = new Shop();Shop x3 = new Shop();Shop x4 = new Shop();Thread t1 = new Thread() {@Overridepublic void run() {x1.m1();}};Thread t2 = new Thread() {@Overridepublic void run() {x1.m1();}};t1.start();t2.start();}
}

内存图

两个线程都会拷贝m1方法,但同一时刻只有一个线程能使用,不能同时执行。如果第二个线程调用x2.m1,则两个线程可以同时进行。因为他俩不是同一个对象。这也证明了非静态方法在每个方法里都有一份,所有在锁的情况下可以调用自己对象里的非静态方法。

对静态方法加锁比如说像x1.m4,x2.m3两线程就不能同时进行。

如果是x1.m1,x2.m5。m5没枷锁,所以他并不遵守规则。

http://www.xdnf.cn/news/520561.html

相关文章:

  • 中山市东区信息学竞赛2025 题目解析
  • CMake调试与详细输出选项解析
  • 基于区块链技术的智能汽车诊断与性能分析
  • 运行vscode编辑器源码
  • 课外活动:再次理解页面实例化PO对象的魔法方法__getattr__
  • 【免杀】C2免杀技术(五)动态API
  • C2S-Scale方法解读
  • [Android] 青木扫描全能文档3.0,支持自动扫描功能
  • 机器学习入门之朴素叶贝斯和决策树分类(四)
  • 【VMware】开启「共享文件夹」
  • 计算机系统的工作原理
  • 2.2.5
  • 进程间通信--信号量【Linux操作系统】
  • leetcode解题思路分析(一百六十四)1418 - 1424 题
  • [论文品鉴] DeepSeek V3 最新论文 之 MHA、MQA、GQA、MLA
  • 进程状态并详解S和D状态
  • C++学习:六个月从基础到就业——C++17:结构化绑定
  • 什么是dom?作用是什么
  • 产品周围的几面墙
  • C++高级用法--绑定器和函数对象
  • 垂直智能体:企业AI落地的正确打开方式
  • [人月神话_6] 另外一面 | 一页流程图 | 没有银弹
  • 三:操作系统线程管理之用户级线程与内核级线程
  • 大模型应用开发工程师
  • 从逻辑学视角探析证据学的理论框架与应用体系;《证据学》大纲参考
  • Java学习手册:服务熔断与降级
  • 朴素贝叶斯
  • 做什么, what to do?
  • 面试题总结二
  • atcoder C - ~