首页 > 编程笔记

Java synchronized实现线程同步

Java 中允许多线程并行访问,即同一个时间段内多个线程同时完成各自的操作。这样就会带来一个问题,当多个变量同时操作一个共享数据时,可能会导致数据不准确的问题。

例如,我们统计多线程用户访问量,并输出访问信息,代码如下:
public class Account implements Runnable{
    private static int num;
    @Override
    public void run() {
        // TODO Auto-generated method stub
        try {
            Thread.currentThread().sleep(1);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        num++;
        System.out.println(Thread.currentThread().getName()+"是当前的第"+num+"位访客");
    }
}

public class AccountTest {
    public static void main(String[] args) {
        Account account = new Account();
        Thread t1 = new Thread(account,"线程1");
        Thread t2 = new Thread(account,"线程2");
        t1.start();
        t2.start();
    }
}
运行结果为:

线程1是当前的第2位访客
线程2是当前的第2位访客

可以看到此时的访问量数据是有问题的,线程 1 和线程 2 都显示为第 2 位访客,什么原因导致数据出错的呢?就是因为两个线程在同时访问静态资源 num。

这就是多线程同时访问共享数据时带来的隐患,项目中一定要解决这个问题。比如金融项目,涉及资金安全的对这部分要求是非常严格的。那么如何来解决这个问题呢?

需要使用线程同步,可以通过 synchronized 修饰方法来实现线程同步,每个 Java 对象都有一个内置锁,内置锁会保护使用 synchronized 关键字修饰的方法,要调用该方法必须先获得内置锁,否则就处于阻塞状态,具体实现如下:
public class Account implements Runnable{
    private static int num;
    @Override
    public synchronized void run() {
        // TODO Auto-generated method stub
        try {
            Thread.currentThread().sleep(1);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        num++;
        System.out.println(Thread.currentThread().getName()+"是当前的第"+num+"位访客");
    }
}
再次运行程序,结果如下:

线程1是当前的第1位访客
线程2是当前的第2位访客

通过结果可以看到现在的访问量数据是正确的,就是因为实现了线程同步的机制。

假设线程 1 先到,获取了 run() 方法的锁,之后线程 2 到了,发现 run() 方法被锁起来了。要调用方法必须拿到锁,但是此时锁被线程 1 获取了,只有当线程 1 调用完方法之后才会释放锁。执行了一次 num++,然后输出信息,之后线程1释放内置锁。线程 2 获取到了内部锁,由阻塞状态进入运行状态,调用 run() 方法,再一次执行 num++ 并输出信息。两个线程是按照先后顺序来执行的,并没有同时去修改 num,所以看到了正确的结果。

synchronized 可以修饰实例方法,也可以修饰静态方法,但是两者在使用上是有区别的。接下来,我们通过实际的例子来学习,现有程序如下:
public class SynchronizedTest {
    public static void main(String[] args) {
        for(int i = 0; i < 5;i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    // TODO Auto-generated method stub
                    SynchronizedTest.test();
                }
            });
            thread.start();
        }
    }
    public static void test() {
        System.out.println("start...");
        try {
            Thread.currentThread().sleep(1000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        System.out.println("end...");
    }
}
运行程序,结果为:

start...
start...
start...
start...
start...
end...
end...
end...
end...
end...

程序运行结果并不是“start...”“end...”成对输出,而是先输出所有的“start...”再输出所有的“end...”。这是什么原因造成的?就是因为多线程并行访问,test() 方法中输出“start...”之后,休眠了 1000ms,所以就给了其他线程执行的机会。1000ms 执行 5 次线程时间上绰绰有余,所以实际的运行情况是 5 个线程都打印了“start...”,然后一起等待 1000ms,再一起输出“end...”。

好了,现在我们给 test() 方法添加 synchronized 关键字,避免多线程并行访问的问题,代码如下:
public synchronized static void test() {
        System.out.println("start...");
        try {
            Thread.currentThread().sleep(1000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        System.out.println("end...");
}
再次运行程序,结果为:

start...
end...
start...
end...
start...
end...
start...
end...
start...
end...

此时的结果是“start...”“end...”成对输出,因为我们给 test() 方法加了一把锁,线程 1 先到,拿到了这把锁,然后执行 test 的业务逻辑。在整个执行过程中,其他线程到了也只能处于阻塞状态等待,因为它们拿不到锁。只有当线程 1 执行完毕释放了锁,其他线程才可以拿到锁进而执行方法。所以当前实际的执行情况是线程 1 输出“start...”,等待 1000ms 之后输出“end...”,紧接着线程 2 输出“start...”,等待 1000ms 之后输出“end...”,以此类推。

上述代码中 synchronized 修饰的是静态方法,现在用 synchronized 修饰实例方法,代码如下:
public class SynchronizedTest {
    public static void main(String[] args) {
        for(int i = 0; i < 5;i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    // TODO Auto-generated method stub
                    SynchronizedTest synchronizedTest = new SynchronizedTest();
                    synchronizedTest.test();
                }
            });
            thread.start();
        }
    }
    public synchronized void test() {
        System.out.println("start...");
        try {
            Thread.currentThread().sleep(1000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        System.out.println("end...");
    }
}
运行结果为:

start...
start...
start...
start...
start...
end...
end...
end...
end...
end...

通过结果可以看到,此时的内置锁并没有为一个线程锁定资源,加锁的效果并没有实现,这是为什么呢?因为我们当前锁定的是一个实例方法,每一个线程都有这样的一个实例方法,相互之间是独立的。即 test 方法并不是被所有线程所共享的,实际的运行情况是每一个线程都获取自己的锁,然后并行访问,相互之间并没有“你运行、我等待”的关系,所以给实例方法添加 synchronized 关键字并不能实现线程同步。

看到这里,大家可能会有疑问了,本节的第一个案例“统计访问量”中,synchronized 修饰的也是实例方法,为什么就可以实现同步呢?因为实例方法中操作的变量 num 是静态的,所以还是多线程在共享资源,线程同步的本质是锁定多个线程所共享的资源。

synchronized 还可以修饰代码块,会为代码块加上内置锁,从而实现同步。在静态方法中添加 synchronized 可以同步代码块,例如:
public class SynchronizedTest {
    public static void main(String[] args) {
        for(int i = 0; i < 5;i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    // TODO Auto-generated method stub
                    SynchronizedTest.test();
                }
            });
            thread.start();
        }
    }
    public static void test() {
        synchronized (SynchronizedTest.class) {
            System.out.println("start...");
            try {
                Thread.currentThread().sleep(1000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            System.out.println("end...");
        }
    }
}
synchronized() 内设置需要加锁的资源,静态方法是属于类的方法,不属于任何一个实例对象。所以静态方法中的 synchronized() 只能锁定类,不能锁定实例,this 可以表示当前的一个实例。


程序会自动报错,静态方法中不能使用 this 关键字。同理静态方法中也不能锁定实例变量,只能锁定静态变量,程序运行结果为:

start...
end...
start...
end...
start...
end...
start...
end...
start...
end...


在实例方法中也可以添加 synchronized 同步代码块,具体实现如下:
public class SynchronizedTest {
    public static void main(String[] args) {
        for(int i = 0; i < 5;i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    // TODO Auto-generated method stub
                    SynchronizedTest synchronizedTest = new SynchronizedTest();
                    synchronizedTest.test();
                }
            });
            thread.start();
        }
    }
    public void test() {
        synchronized (this) {
            System.out.println("start...");
            try {
                Thread.currentThread().sleep(1000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            System.out.println("end...");
        }
    }
}
运行结果为:

start...
start...
start...
start...
start...
end...
end...
end...
end...
end...

通过结果可以得知没有实现代码同步,原因是 synchronized() 锁定的是 this,即当前的实例对象。每个线程都有一个实例对象,相互独立,并不是共享资源,所以没有实现线程同步,该如何修改呢?

只需要锁住共享资源即可,实例对象是每个线程独有的,类则是共享的,所以锁定类即可,代码如下:
public void test() {
    synchronized (SynchronizedTest.class) {
        System.out.println("start...");
        try {
            Thread.currentThread().sleep(1000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        System.out.println("end...");
    }
}
运行结果为:

start...
end...
start...
end...
start...
end...
start...
end...
start...
end...

推荐阅读