并发编程(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);
}
final
和volatile
在防止指令重排序上有相似之处
防止重排序 :二者都能在一定程度上防止指令重排序。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只保证读正确,不保证写正确。在子线程读的时候,所有子线程读都是对的,但往回写会互相覆盖。
volatile
和synchronized
volatile
和synchronized
是两个不同的同步机制,它们的使用范围和作用确实有明显区别:
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没枷锁,所以他并不遵守规则。