java高并发、多线程(三)
synchronized
先看下面例子:
public class SynchronizedTest {
static int count = 0;
public static void add() {
for (int i = 0; i < 10000; i++) {
count++;
}
}
public static void main(String[] args) {
List<Thread> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(new Thread(SynchronizedTest::add,"Thread-"+i));
}
list.forEach(t->t.start());
list.forEach(t-> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(count);
}
}
运行结果:
以上代码开启10个线程对count进行累加10000次,我们希望的结果是10万,但是最终结果是小于等于10万的。这是由于count++这个操作并不是原子操作,代码产生了同步问题,我们可以给相关代码加上锁来解决上面的问题。
加锁的方式
- 同步代码块上加锁
private static Object object = new Object();
public static void add() {
for (int i = 0; i < 10000; i++) {
synchronized (object){
count++;
}
}
}
- 方法上加锁
public synchronized static void add() {
for (int i = 0; i < 10000; i++) {
count++;
}
}
运行结果:
synchronized的性质
- 保证线程间可见性
static int count = 0;
我们可以观察到上述代码中,我们并没有给count变量采用volatile修饰,但是程序的运行结果是正确的,那么说明synchronized可以替代volatile,能够保证线程间的可见性。
那volatile可以替代synchronized吗?
我们对上述代码做如下修改:
volatile static int count = 0;
public /*synchronized*/ static void add() {
for (int i = 0; i < 10000; i++) {
count++;
}
}
运行结果:
由运行结果可知:volatile并不能替代synchronized的作用,它不能保证多个线程共同修改共享变量所带来的不一致问题。
- synchronized获得的是重入锁
修改上述代码:
static int count = 0;
public synchronized static void add() {
for (int i = 0; i < 10000; i++) {
count++;
}
//调用另一个同步方法
sub();
}
public synchronized static void sub() {
for (int i = 0; i < 10000; i++) {
count--;
}
}
运行结果:
从运行结果可以知道,一个同步方法可以调用另一个同步方法。
synchronized锁升级
优化
- 采用细粒度的锁,减少同步代码块中代码量
注意事项
- 不使用字符串作为锁对象
例如:
public class SynchronizedTest {
String lock1 = "lock";
String lock2 = "lock";
public void m1(){
synchronized (lock1){
System.out.println("m1 start");
while(true){}
}
}
public void m2(){
synchronized (lock2){
System.out.println("m2 start");
while(true){}
}
}
public static void main(String[] args) {
SynchronizedTest st = new SynchronizedTest();
new Thread(st::m1).start();
new Thread(st::m2).start();
}
}
运行结果:
由上面的例子可以看出:代码原意是想m1与m2方法获取不同的锁,但是由于string常量池的性质,导致两把锁实际上指向的是同一个对象,从而m1与m2方法仅能执行其中的一个。
- 不要改变锁的引用
例如:
public class SynchronizedTest {
private Object o = new Object();
public void m1(){
synchronized (o){
System.out.println("m1 start");
while(true){}
}
}
public void m2(){
synchronized (o){
System.out.println("m2 start");
while(true){}
}
}
public static void main(String[] args) {
SynchronizedTest st = new SynchronizedTest();
new Thread(st::m1).start();
try {
Thread.sleep(1000);
//改变o指向的对象
st.o = new Object();
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(st::m2).start();
}
}
运行结果:
从这个例子可以看出,当改变了锁对象的引用后m2方法可以得到执行。因此锁对象建议加上final关键字修饰
private final Object o = new Object();