Java多线程编程二(synchronized同步方法和synchronized同步代码块)


本文主要讲述如何解决非线程安全问题,感谢java多线程核心编程一书,为本系列文章提供参考借鉴

一、synchronized同步方法
1.方法内的变量为线程安全
“非线程安全“问题存在与实例变量中,如果是方法内部的私有变量,则不存在“非线程安全“问题,也就是线程安全的。
public class MyThread extends Thread {
    @Override
    public void run() {
        int count = 5;
        System.out.println("需要执行的任务");
        while (count > 0) {
            count--;
            System.out.println("由:" + Thread.currentThread().getName() + "计算,count=" + count);
        }
    }
    
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        Thread a = new Thread(myThread, "A");
        Thread b = new Thread(myThread, "B");
        Thread c = new Thread(myThread, "C");
        a.start();
        b.start();
        c.start();
        System.out.println("运行结束!");
    }

}

运行结果:
在这里插入图片描述
尽管打印的内容不规则,但是线程安全的。

2. 实例变量非线程安全

如果多个线程共同访问同一个对象中的实例变量,则可能出现“非线程安全问题“。
更改上面的程序,将方法内私有变量更改为,自定义线程的私有变量。如下:

public class MyThread extends Thread {

    private int count = 5;

    @Override
    public void run() {
        System.out.println("需要执行的任务");
            count--;
            System.out.println("由:" + Thread.currentThread().getName() + "计算,count=" + count);
    }
     public static void main(String[] args) {
     MyThread myThread = new MyThread();
     Thread a = new Thread(myThread, "A");
     Thread b = new Thread(myThread, "B");
     Thread c = new Thread(myThread, "C");
     a.start();
     b.start();
     c.start();
     System.out.println("运行结束!");
 }
}

结果如下:
在这里插入图片描述
结果出现了非线程安全问题,假如这个是抢票,则会出现大问题了,解决此问题只需要在方法(run方法)前加上synchronized关键字即可,更改后运行结果为:
在这里插入图片描述
没有在出数据错误的情况了,这样转换为同步方法就解决的线程安全问题。

3.多个对象多个锁

synchronized取得的锁都是对象锁,而不是把一段代码或方法(函数)当作锁,当多个线程同时访问同步方法时,哪个线程先执行同步方法,哪个线程就持有该方法所属对象的锁lock,其他线程只能等待持有锁的线程访问完毕,在进行争夺锁。

4. synchronized方法与所对象
  • 调用关键字synchronized声明的方法一定排队运行的,另外需要牢记“共享“两个字,只有共享资源的读写访问才需要同步化,如果不是共享的资源,就没必要使用同步的必要了。
  • 线程A和线程B,如果A现持有了对象的Lock锁访问synchronized方法,那么B无法访问被此同步方法了,若一定要访问此同步方法则需等待了,但是B可以异步调用非synchronized类型的方法。
5. 脏读

为了避免数据出现交叉、出错,使用synchronized关键字来进行同步,虽然在赋值的时候进行同步,但在取值的时候有可能出现一些意想不到的意外,这种情况就是脏读,发生脏读的情况是在读取实例变量时,此值已经被其他线程更改过了。解决办法就是在取值的方法上加上synchronized关键字使其成为同步方法,这样就必须等待其他一系列的操作完成后,在进行读取,这样就避免来脏读。总结:
1:当A线程调用anyObject对象加入synchronized关键字的X方法时,A线程就获得来X方法锁,更准备地讲,是获得了对象的锁,所以其他线程必须等A线程执行完才可以调用X方法,但B线程可以随意调用其他的非synchronized同步方法。
2:当A线程调用anyObject对象加入synchronized关键字的X方法时,A线程就获得来X方法所在对象的锁,所以其他线程必须等A线程执行完毕才可以调用X方法,而B线程如果调用声明来synchronized关键字的非X方法时,必须等A线程将X方法还行完,也就是释放对象锁后才可以调用。这是A线程已经执行来一个完整的任务,也就是说一些变量的赋值已经完成不尊在脏读的环境了。

6.synchronized锁重入
  • 关键字synchronized拥有锁重入的功能,也就是在使用synchronized时,当一个线程得到一个对象锁后,再次请求此对象锁时是可以再次得到该对象的锁的。一个synchronized方法/快的内部调用本类的其他synchronized方法/快时,是永远可以得到锁的。
7.出现异常锁自动释放
  • 线程a持用对象的锁,此时发生异常则a会是释放锁,以便其他线程能够成功拿到锁来进行访问。
8.同步不具有继承性
  • 同步不能继承,如果子类重写了父类的同步方法,则需要在子类的同步方法中添加synchronized关键字,否则无法同步。
二、同步语句块

用关键字synchronized声明方法在某些情况下是有弊端的,比如A线程调用同步方法执行了一个长时间的任务,那么B线程则必须等待较长时间。在这样的情况下就要考虑使用synchronized同步代码块来解决来。

1.synchronized同步代码块的使用
public class MyService {

    public void serviceMethod() {

        try {
            synchronized (this) {//使用同步代码块
                System.out.println("begin time = " + new Date());
                Thread.sleep(2000);
                System.out.println("end   time = " + new Date());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    static class ThreadA extends Thread {
        private MyService myService;

        ThreadA(MyService myService) {
            this.myService = myService;
        }

        @Override
        public void run() {
            super.run();
            myService.serviceMethod();
        }
    }

    static class ThreadB extends Thread {
        private MyService myService;

        ThreadB(MyService myService) {
            this.myService = myService;
        }

        @Override
        public void run() {
            super.run();
            myService.serviceMethod();
        }
    }
    public static void main(String[] args) {

        MyService myService = new MyService();

        MyService.ThreadA threadA = new MyService.ThreadA(myService);

        threadA.setName("a");

        threadA.start();

        MyService.ThreadB threadB = new MyService.ThreadB(myService);

        threadB.setName("b");

        threadB.start();

    }


}

运行结果为:
在这里插入图片描述
结果来看并未出现非线程安全问题,但是执行效率确实很低并没有提高,执行的效果还是同步运行的。

2.用同步代码块解决同步方法的弊端(一半同步,一半异步)

修改上面MyService中serviceMethod()方法,其他不变:

public void serviceMethod() {
        for (int i = 0; i < 20; i++) {
            System.out.println("nosynchronized threadName=" + Thread.currentThread().getName() + "  i=" + (i + 1));
        }
        System.out.println("");
        synchronized (this) {
            for (int i = 0; i < 20; i++) {
                System.out.println("synchronized threadName=" + Thread.currentThread().getName() + "  i=" + (i + 1));
            }
        }
    }

运行结果为:

nosynchronized threadName=a  i=1
nosynchronized threadName=a  i=2
nosynchronized threadName=a  i=3
nosynchronized threadName=a  i=4
nosynchronized threadName=a  i=5
nosynchronized threadName=a  i=6
nosynchronized threadName=a  i=7
nosynchronized threadName=a  i=8
nosynchronized threadName=a  i=9
nosynchronized threadName=a  i=10
nosynchronized threadName=a  i=11
nosynchronized threadName=a  i=12
nosynchronized threadName=a  i=13
nosynchronized threadName=a  i=14
nosynchronized threadName=a  i=15
nosynchronized threadName=a  i=16
nosynchronized threadName=a  i=17
nosynchronized threadName=a  i=18
nosynchronized threadName=a  i=19
nosynchronized threadName=a  i=20

synchronized threadName=a  i=1
synchronized threadName=a  i=2
synchronized threadName=a  i=3
synchronized threadName=a  i=4
synchronized threadName=a  i=5
synchronized threadName=a  i=6
synchronized threadName=a  i=7
synchronized threadName=a  i=8
synchronized threadName=a  i=9
synchronized threadName=a  i=10
synchronized threadName=a  i=11
synchronized threadName=a  i=12
synchronized threadName=a  i=13
synchronized threadName=a  i=14
synchronized threadName=a  i=15
synchronized threadName=a  i=16
synchronized threadName=a  i=17
synchronized threadName=a  i=18
synchronized threadName=a  i=19
synchronized threadName=a  i=20
nosynchronized threadName=b  i=1
nosynchronized threadName=b  i=2
nosynchronized threadName=b  i=3
nosynchronized threadName=b  i=4
nosynchronized threadName=b  i=5
nosynchronized threadName=b  i=6
nosynchronized threadName=b  i=7
nosynchronized threadName=b  i=8
nosynchronized threadName=b  i=9
nosynchronized threadName=b  i=10
nosynchronized threadName=b  i=11
nosynchronized threadName=b  i=12
nosynchronized threadName=b  i=13
nosynchronized threadName=b  i=14
nosynchronized threadName=b  i=15
nosynchronized threadName=b  i=16
nosynchronized threadName=b  i=17
nosynchronized threadName=b  i=18
nosynchronized threadName=b  i=19
nosynchronized threadName=b  i=20

synchronized threadName=b  i=1
synchronized threadName=b  i=2
synchronized threadName=b  i=3
synchronized threadName=b  i=4
synchronized threadName=b  i=5
synchronized threadName=b  i=6
synchronized threadName=b  i=7
synchronized threadName=b  i=8
synchronized threadName=b  i=9
synchronized threadName=b  i=10
synchronized threadName=b  i=11
synchronized threadName=b  i=12
synchronized threadName=b  i=13
synchronized threadName=b  i=14
synchronized threadName=b  i=15
synchronized threadName=b  i=16
synchronized threadName=b  i=17
synchronized threadName=b  i=18
synchronized threadName=b  i=19
synchronized threadName=b  i=20

由结果可知,当一个线程访问object的一个synchronized同步代码块时,另一个线程仍然可以访问该object对象中的非synchronized(this)同步代码块。有效的提高了运行效率。

3.synchronized代码块间的同步性
  • 在使用synchronized(this)代码块时需要注意的时,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对同一个object中所有其他synchronized(this)同步代码块的访问将被阻塞,这说明synchronized使用的“对象监视器“是同一个。
  • 和synchronized方法一样,synchronized(this)代码块是锁定当前对象的。
4.将任意对象作为对象监视器

多个线程调用同一个对象中的不同名称的synchronized同步方法或synchronized(this)时,调用效果就是按顺序进行,也就是同步,阻塞的。

  • 无论是synchronized(this)代码块还是synchronized同步方法,同一时间只有一个线程可以执行synchronized(this)代码块中的代码或synchronized同步方法,对其他synchronized(this)代码块中的代码或synchronized同步方法的调用都是呈阻塞状态的。

java中支持对“任意对象“作为“对象监视器“来实现同步的功能。这个对象大多是实例变量及方法中的参数,使用格式synchronized(非this对象x)

  • 在多个线程持有“对象监视器“作为同一个对象是,同一时间只有一个线程可以执行synchronized(非this对象x)同步代码中的代码。
  • 当持有“对象监视器“为同一个对象的前提下,同一时间只有一个线程可以执行synchronized(非this对象x)同步代码中的代码
  • synchronized(非this对象x)优点:在一个类中有多个synchronized方法,这时虽然能实现同步,但是会收到阻塞,所以影响效率;但如果使用synchronized(非this对象x)同步代码块,则synchronized(非this对象x)代码块中的代码和synchronized同步方法是异步的,不会和其他this锁同步方法或同步代码块争抢this锁,可以大大提高运行效率。
  • synchronized(非this对象x)同步代码块中非this对象x不同时,则是异步效果不会阻塞。
  • 同步代码块放在非同步synchronized方法中声明,并不能保证调用方法的线程的执行同步/顺序行,也就是线程调用方法的顺序是无需的,虽然同步块中执行的顺序是同步的,这样极容易出现脏读,使用synchronized(非this对象x)同步代码块的格式可以解决这个脏读问题。
public class MyService {

    public void serviceMethod(MyList myList,String data) {
        try {
            if (myList.getSize() < 1) {
                Thread.sleep(2000);//模拟长时间取数据
                myList.add(data);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }


    static class ThreadA extends Thread {
        private MyList myList;

        ThreadA(MyList myList) {
            this.myList = myList;
        }
        @Override
        public void run() {
            super.run();
            new MyService().serviceMethod(myList,"threadA");
        }
    }
    static class ThreadB extends Thread {
        private MyList myList;
        ThreadB(MyList myList) {
            this.myList = myList;
        }
        @Override
        public void run() {
            super.run();
            new MyService().serviceMethod(myList,"threadB");
        }
    }

    static class MyList{
        private List list = new ArrayList<String>();
        synchronized public void add(String data) {
            list.add(data);
        }
        synchronized public int getSize() {
            return list.size();
        }
    }
  public static void main(String[] args) throws InterruptedException {

   MyService.MyList myList = new MyService.MyList();

   MyService.ThreadA threadA = new MyService.ThreadA(myList);

   threadA.setName("a");

   threadA.start();

   MyService.ThreadB threadB = new MyService.ThreadB(myList);

   threadB.setName("b");

   threadB.start();

   Thread.sleep(6000);

   System.out.println("listSize="+myList.getSize());

}
}

运行结果:

listSize=2
如结果显示出现来脏读,程序脱离来我们的本意,使用synchronized(非this对象x)即可解决这个问题,代码修改如下:

//由于myList参数对象在项目中是一份实例,单例的,而且要对myList参数的getSize()方法做同步的调用,所以就对myList参数进行同步处理
//在MyServer中加入synchronized(非this对象x):
public void serviceMethod(MyList myList, String data) {
        try {
            synchronized (myList) {
                if (myList.getSize() < 1) {
                    Thread.sleep(2000);//模拟长时间取数据
                    myList.add(data);
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
5.对synchronized(非this对象x)三个结论

" synchronized(非this对象x)"格式的写法是将X对象本身作为“对象监视器“

  • 当多个线程同时执行synchronized(非this对象x){}同步代码块时呈同步效果。
  • 当其他线程执行x对象中synchronized同步方法是呈同步效果
  • 当其他线程执行x对象方法里面的synchronized(this)代码块时也呈现同步效果。

注意:如果其他线程待用不加synchronized关键字的方法或者同步代码块时还是异步调用。

6.静态同步synchronized方法与synchronized(class)代码块和数据烈性String的常量池性
  • 关键字synchronized还可以应用在static静态方法上,如果这样写,那是对当前的*.java文件对Class类进行持锁。它和加在非static静态方法上的和代码块的作用是一样的

  • 在jvm中具有String常量缓存的功能,将synchronized(非this对象x)同步块与String联合使用时,要注意String a="AA" ; String b="AA"; //由于常量池特殊性 a和b是想等的,所以在两个线程分别持有对象锁a和对象锁b时,他们其实持有的对象锁时同一个“AA“,所有在其中一个线程持有锁时,另一个线程时阻塞的无法执行相应的程序。所以在大多数的情况下,同步synchronized(非this对象x)代码块都不使用String作为锁对象。而改用其他。

  • 同步方法容易造成死循环。无论是两个或者多个synchronized方法有关联或无关联,若其中一个synchronized方法无限执行中,就会造成其他线程的无限等待。采用synchronized同步代码块持有不同的锁类型即可解决。

  • 对于class A{.....},A a=new A(),synchronized(a){},如果此时对象锁a未释放,而其他线程调用a的同步非静态方法,则阻塞,等待,知道对象锁a释放。

7.锁对象的更改

例:

public class MyService {

    private String lock = "123";

    public void serviceMethod() {
        try {
            synchronized (lock) {
                System.out.println(Thread.currentThread().getName() + " begin " + System.currentTimeMillis());
                lock = "456";
                Thread.sleep(2000);
                System.out.println(Thread.currentThread().getName() + " end " + System.currentTimeMillis());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }


    static class ThreadA extends Thread {
        private MyService myService;
        ThreadA(MyService myService) {
            this.myService = myService;
        }
        @Override
        public void run() {
            myService.serviceMethod();
        }
    }

    static class ThreadB extends Thread {
        private MyService myService;
        ThreadB(MyService myService) {
            this.myService = myService;
        }
        @Override
        public void run() {
            myService.serviceMethod();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        MyService myService = new MyService();
        MyService.ThreadA threadA = new MyService.ThreadA(myService);
        threadA.setName("A");
        MyService.ThreadB threadB = new MyService.ThreadB(myService);
        threadB.setName("B");
        threadA.start();
        Thread.sleep(50);
        threadB.start();
    }
}

结果为:在这里插入图片描述
由于线程阻塞了50毫秒,导致ThreadB运行时持有的锁变成了“456“,而ThreadA仍然是“123“,所以两个线程呈异步。去掉睡眠50毫秒这行代码多运行几次,发现结果有时同步有时异步,并不一定,也就是说和去不去掉50毫秒的关系并不起决定行作用,只要线程ThreadB在lock变成“456“之前运行,那么结果就是同步则两个线程获取的锁都为“A“,在其之后就是异步了,两个线程获取的锁分别为“123“和“456“,这也是线程运行随机性造成的,在第一篇介绍过。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值