【Java多线程-6】synchronized同步锁

Monitor 是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联(对象头的MarkWord中的LockWord指向monitor的起始地址),同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。

其结构如下:

在这里插入图片描述

  • Owner:初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL。

  • EntryQ:关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor record失败的线程。

  • RcThis:表示blocked或waiting在该monitor record上的所有线程的个数。

  • Nest:用来实现重入锁的计数。HashCode:保存从对象头拷贝过来的HashCode值(可能还包含GC age)。

  • Candidate:用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降。Candidate只有两种可能的值,0表示没有需要唤醒的线程,1表示要唤醒一个继任线程来竞争锁。

2 synchronized 使用

============================================================================

synchronized是Java中的关键字,是一种同步锁,它修饰的对象有以下几种:

| 序号 | 类别 | 作用范围 | 作用对象 |

| — | — | — | — |

| 1 | 同步代码块 | 被synchronized修饰的代码块 | 调用这个代码块的单个对象 |

| 2 | 同步方法 | 被synchronized修饰的方法 | 调用该方法的单个对象 |

| 3 | 同步静态方法 | 被synchronized修饰的静态方法 | 静态方法所属类的所有对象 |

| 4 | 同步类 | 被synchronized修饰的代码块 | 该类的所有对象 |

2.1 同步代码块


同步代码块就是将需要的同步的代码使用同步锁包裹起来,这样能减少阻塞,提高程序效率。

同步代码块格式如下:


    synchronized(对象){

    	同步代码;

    }



同样对于文章开头卖票的例子,进行线程安全改造,代码如下:


public class SellTickets {

    public static void main(String[] args) {

        TicketSeller seller = new TicketSeller();



        Thread t1 = new Thread(seller, "窗口1");

        Thread t2 = new Thread(seller, "窗口2");

        Thread t3 = new Thread(seller, "窗口3");



        t1.start();

        t2.start();

        t3.start();

    }

}



class TicketSeller implements Runnable {

    private static int tickets = 100;



    @Override

    public void run() {

        while (true) {

            synchronized (this) {

                try {

                    Thread.sleep(10);

                    if (tickets > 0) {

                        System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "张票");

                    }

                } catch (InterruptedException e) {

                    e.printStackTrace();

                }

            }

        }

    }

}



同步代码块的关键在于锁对象,多个线程必须持有同一把锁,才会实现互斥性。

将上面代码中的 synchronized (this) 改为 synchronized (new Objcet()) 的话,线程安全将得不到保证,因为两个线程的持锁对象不再是同一个。

又比如下面这个例子:


public class SyncTest implements Runnable {

    // 共享资源变量

    int count = 0;



    @Override

    public void run() {

        synchronized (this) {

            for (int i = 0; i < 5; i++) {

                System.out.println(Thread.currentThread().getName() + ":" + count++);

                try {

                    Thread.sleep(1000);

                } catch (InterruptedException e) {

                    e.printStackTrace();

                }

            }

        }

    }



    public static void main(String[] args) {

//        test1();

        test2();

    }



    public static void test1() {

        SyncTest syncTest1 = new SyncTest();

        Thread thread1 = new Thread(syncTest1, "thread-1");

        Thread thread2 = new Thread(syncTest1, "thread-2");

        thread1.start();

        thread2.start();

    }



    public static void test2() {

        SyncTest syncTest1 = new SyncTest();

        SyncTest syncTest2 = new SyncTest();



        Thread thread1 = new Thread(syncTest1, "thread-1");

        Thread thread2 = new Thread(syncTest2, "thread-2");

        thread1.start();

        thread2.start();

    }

}



从输出结果可以看出,test2() 方法无法实现线程安全,原因在于我们指定锁为this,指的就是调用这个方法的实例对象,然而 test2() 实例化了两个不同的实例对象 syncTest1,syncTest2,所以会有两个锁,thread1与thread2分别进入自己传入的对象锁的线程执行 run() 方法,造成线程不安全。

如果要使用这个经济实惠的锁并保证线程安全,那就不能创建出多个不同实例对象。如果非要想 new 两个不同对象出来,又想保证线程同步的话,那么 synchronized 后面的括号中可以填入SyncTest.class,表示这个类对象作为锁,自然就能保证线程同步了。


synchronized(xxxx.class){

  //todo

}



一个线程访问一个对象的synchronized代码块时,别的线程可以访问该对象的非synchronized代码块而不受阻塞。

例如下面的例子:


public class SyncTest {

    public static void main(String[] args) {

        Counter counter = new Counter();

        Thread thread1 = new Thread(counter, "线程-1");

        Thread thread2 = new Thread(counter, "线程-2");

        thread1.start();

        thread2.start();

    }

}



class Counter implements Runnable {

    private int count = 0;



    public void countAdd() {

        synchronized (this) {

            for (int i = 0; i < 5; i++) {

                try {

                    System.out.println(Thread.currentThread().getName() + " 同步计数:" + (count++));

                    Thread.sleep(1000);

                } catch (InterruptedException e) {

                    e.printStackTrace();

                }

            }

        }

    }



    public void printCount() {

        for (int i = 0; i < 5; i++) {

            try {

                System.out.println(Thread.currentThread().getName() + " 非同步输出:" + count);

                Thread.sleep(1000);

            } catch (InterruptedException e) {

                e.printStackTrace();

            }

        }

    }



    public void run() {

        String threadName = Thread.currentThread().getName();

        if (threadName.equals("线程-1")) {

            countAdd();

        } else if (threadName.equals("线程-2")) {

            printCount();

        }

    }

}



我们也可以用synchronized 给对象加锁。这时,当一个线程访问该对象时,其他试图访问此对象的线程将会阻塞,直到该线程访问对象结束。也就是说谁拿到那个锁谁就可以运行它所控制的那段代码,,例如下例:


public class SyncTest {

    public static void main(String args[]) {

        Account account = new Account("zhang san", 10000.0f);

        AccountOperator accountOperator = new AccountOperator(account);



        final int THREAD_NUM = 5;

        Thread threads[] = new Thread[THREAD_NUM];

        for (int i = 0; i < THREAD_NUM; i++) {

            threads[i] = new Thread(accountOperator, "Thread-" + i);

            threads[i].start();

        }

    }

}



class Account {

    String name;

    double amount;



    public Account(String name, double amount) {

        this.name = name;

        this.amount = amount;

    }



    //存钱

    public void deposit(double amt) {

        amount += amt;

        try {

            Thread.sleep(0);

        } catch (InterruptedException e) {

            e.printStackTrace();

        }

    }



    //取钱

    public void withdraw(double amt) {

        amount -= amt;

        try {

            Thread.sleep(0);

        } catch (InterruptedException e) {

            e.printStackTrace();

        }

    }



    public double getBalance() {

        return amount;

    }

}



class AccountOperator implements Runnable {

    private Account account;



    public AccountOperator(Account account) {

        this.account = account;

    }



    public void run() {

        synchronized (account) {

            String name = Thread.currentThread().getName();

            account.deposit(500);

            System.out.println(name + "存入500,最新余额:" + account.getBalance());

            account.withdraw(400);

            System.out.println(name + "取出400,最新余额:" + account.getBalance());

            System.out.println(name + "最终余额:" + account.getBalance());

        }

    }

}



同步锁可以使用任意对象作为锁,当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的对象来充当锁:


class Test implements Runnable {

   private byte[] lock = new byte[0];  // 特殊的instance变量

   public void method() {

      synchronized(lock) {

         // todo 同步代码块

      }

   }

 

   public void run() {

 

   }

}



2.2 同步方法


Synchronized修饰一个方法很简单,就是在方法的前面加synchronized,synchronized修饰方法和修饰一个代码块类似,只是作用范围不一样,修饰代码块是大括号括起来的范围,而修饰方法范围是整个函数。


public synchronized void method(){

   // todo

}



下面用同步函数的方式解决售票场景的线程安全问题,代码如下:


public class SellTickets {

    public static void main(String[] args) {

        TicketSeller seller = new TicketSeller();



        Thread t1 = new Thread(seller, "窗口1");

        Thread t2 = new Thread(seller, "窗口2");

        Thread t3 = new Thread(seller, "窗口3");



        t1.start();

        t2.start();

        t3.start();

    }

}



class TicketSeller implements Runnable {

    private static int tickets = 100;



    @Override

    public void run() {

        while (true) {

            sellTickets();

        }

    }



    public synchronized void sellTickets() {

        try {

            Thread.sleep(10);

            if (tickets > 0) {

                System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "张票");

            }

        } catch (InterruptedException e) {

            e.printStackTrace();

        }

    }

}



同步方法有以下特征:

线程、数据库、算法、JVM、分布式、微服务、框架、Spring相关知识

一线互联网P7面试集锦+各种大厂面试集锦

学习笔记以及面试真题解析

true) {

        sellTickets();

    }

}



public synchronized void sellTickets() {

    try {

        Thread.sleep(10);

        if (tickets > 0) {

            System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "张票");

        }

    } catch (InterruptedException e) {

        e.printStackTrace();

    }

}

}




同步方法有以下特征:




#### 线程、数据库、算法、JVM、分布式、微服务、框架、Spring相关知识

[外链图片转存中...(img-TB5IlpFA-1720021138314)]

#### 一线互联网P7面试集锦+各种大厂面试集锦

[外链图片转存中...(img-81qzO9mX-1720021138314)]

#### 学习笔记以及面试真题解析

[外链图片转存中...(img-5ngyDdZs-1720021138315)]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值