同步代码块
当多个线程使用同一个共享资源时,可以将处理共享资源的代码放在一个使用synchronized关键字来修饰的代码块中,这个代码块被称作同步代码块,格式如下:
synchronized(对象) {
需要被同步的代码
}
对象如同锁,持有锁的线程可以在同步中执行,没有持有锁的线程即使获取CPU的执行权,也进不去,因为没有锁。火车上的卫生间就是一个经典的同步例子,对于简单的卖票程序,加入同步之后的代码为:
class SaleTicket implements Runnable
{
private int tickets = 300;
Object obj = new Object();
public void run()
{
while (true)
{
synchronized (new Object())
{
if (tickets > 0)
{
try {Thread.sleep(10);} catch (InterruptedException e) {}//让线程到这里稍微停一下。
System.out.println(Thread.currentThread().getName() + "...." + tickets--);
}
}
}
}
}
class TicketDemo3
{
public static void main(String[] args)
{
//线程任务对象。
SaleTicket t = new SaleTicket();
//创建四个线程。通过Thread类的对象。
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
同步的前提
有可能出现这样一种情况,多线程安全问题出现后,加入了同步机制,没有想到,安全问题依旧!这时咋办呢?要想到肯定是同步出现了问题,我们只要遵守了同步的前提,就可以解决。同步的前提:
- 必须要有两个或者两个以上的线程;
- 必须是多个线程使用同一个锁。
必须保证同步中只有一个线程在运行。同步在目前的情况下保证了一次只能有一个线程在执行,其他线程进不来,这就是同步的锁机制。
同步的好处
解决了多线程的安全问题。
同步的弊端
当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。
同步函数
例,银行有一个金库,有两个储户,分别存300元,每次存100元,存3次。试问如下程序是否有安全问题,如果有,该如何解决?
class Bank
{
private int sum;
public void add(int n)
{
sum = sum + n;
try {Thread.sleep(10);} catch (InterruptedException e) {}
System.out.println("sum = " + sum);
}
}
class Customer implements Runnable
{
private Bank b = new Bank();
public void run()
{
for (int x = 0; x < 3; x++)
{
b.add(100);
}
}
}
class BankDemo
{
public static void main(String[] args)
{
//1,创建任务对象。
Customer c = new Customer();
Thread t1 = new Thread(c);
Thread t2 = new Thread(c);
t1.start();
t2.start();
}
}
以上程序绝壁是有问题的,那我们又该如何找到问题?我们只要明确以下几点就能找到问题。
- 明确哪些代码是多线程运行代码;
- 明确共享数据;
- 明确多线程运行代码中哪些语句是操作共享数据的。
同步代码块,咱已经很熟了,所以就没必要再用它了,不妨我们试试同步函数,同步函数其实就是在函数上加上了同步关键字进行修饰。
class Bank
{
private int sum;
public synchronized void add(int n)
{
sum = sum + n;
try {Thread.sleep(10);} catch (InterruptedException e) {}
System.out.println("sum = " + sum);
}
}
class Customer implements Runnable
{
private Bank b = new Bank();
public void run()
{
for (int x = 0; x < 3; x++)
{
b.add(100);
}
}
}
class BankDemo
{
public static void main(String[] args)
{
//1,创建任务对象。
Customer c = new Customer();
Thread t1 = new Thread(c);
Thread t2 = new Thread(c);
t1.start();
t2.start();
}
}
这里我们思考一个问题,同步函数用的是哪一个锁呢? 答案是函数需要被对象调用,那么函数都有一个所属对象引用,就是this,所以同步函数使用的锁是this。
验证同步函数使用的锁是this
需求:验证同步函数使用的锁是this。
我们可以这样做,启动两个线程,一个线程负责执行同步代码块(使用明锁),另一个线程使用同步函数(使用this锁)。两个线程执行的任务都是一样的(例如都是卖票),如果它们没有使用相同的锁,说明它们没有同步,会出现数据错误。下面我们就以卖票程序来验证该需求。
class Ticket implements Runnable {
private int tick = 100;
Object obj = new Object();
//定义一个boolean标记。
boolean flag = true;
public void run() {
if(flag) {
while(true) {
synchronized(this) {
if(tick > 0) {
try { Thread.sleep(10); } catch(Exception e) {}
System.out.println(Thread.currentThread().getName()+"...code:"+tick--);
}
}
}
} else {
while(true)
show();
}
}
public synchronized void show() { // this
if(tick > 0) {
try { Thread.sleep(10); } catch(Exception e) {}
System.out.println(Thread.currentThread().getName()+"...show...:"+tick--);
}
}
}
class ThisLockDemo {
public static void main(String[] args) {
Ticket t = new Ticket();
Thread t1 = new Thread(t); // 创建一个线程
Thread t2 = new Thread(t); // 创建一个线程
t1.start();
// 主线程停10ms,此刻能运行的线程只有t1
try { Thread.sleep(10); } catch(Exception e) {}
t.flag = false;
// 主线程醒了之后,t1和t2线程同时运行
t2.start();
}
}
验证static同步函数的锁是类名.class
现在我们思考如果同步函数被静态修饰后,使用的锁是什么呢?通过验证,发现不再是this,因为静态方法中也不可以定义this。静态进内存时,内存中没有本类对象,但是一定有该类对应的字节码文件对象:类名.class,该对象的类型是Class。静态的同步方法,使用的锁是该方法所在类的字节码文件对象,即类名.class。
class Ticket implements Runnable {
private static int tick = 100;
// Object obj = new Object();
boolean flag = true;
public void run() {
if(flag) {
while(true) {
synchronized(Ticket.class) {
if(tick > 0) {
try { Thread.sleep(10); } catch(Exception e) {}
System.out.println(Thread.currentThread().getName()+"...code:"+tick--);
}
}
}
} else {
while(true)
show();
}
}
public static synchronized void show() {
if(tick > 0) {
try { Thread.sleep(10); } catch(Exception e) {}
System.out.println(Thread.currentThread().getName()+"...show...:"+tick--);
}
}
}
class StaticMethodDemo {
public static void main(String[] args) {
Ticket t = new Ticket();
Thread t1 = new Thread(t); // 创建一个线程
Thread t2 = new Thread(t); // 创建一个线程
t1.start();
// 主线程停10ms,此刻能运行的线程只有t1
try { Thread.sleep(10); } catch(Exception e) {}
t.flag = false;
// 主线程醒了之后,t1和t2线程同时运行
t2.start();
}
}
同步函数和同步代码块的区别
- 同步代码块使用的是任意的对象作为锁;
- 同步函数只能使用this作为锁。
同步函数和同步代码块的应用场景
如果说一个类中只需要一个锁,这时可以考虑同步函数,使用this,写法简单。但是,一个类中如果需要多个锁,还有多个类中使用同一个锁,这时只能使用同步代码块。建议使用同步代码块。