java多线程下的对象及变量的并发访问
上一节讲到,并发访问的时候,因为是多线程,变量如果不加锁的话,会出现“脏读”的现象,这个时候需要“临界区”的出现去解决多线程的安全的并发访问。(这个“脏读”的现象不会出现在方法内部的私有变量中,因为其私有的特性,永远都是线程安全的)
目前锁有三种:synchronized / volatile / Lock
三类锁各有所长,本节先介绍关键字 :synchronized
synchronized关键字用来实现线程之间同步互斥。
public class Test{ private num = 0; public void addId(String username){ try { if(username.equals("a")){ num = 100; System.out.println("a set over!"); Thread.sleep(2000); } else { num = 200; System.out.println("b set over!"); } System.out.println(username + " num = " + num); }catch (InterruptedException e){ e.printStackTrace(); } } } public class ThreadA extends Thread { private Test test; public ThreadA(Test test){ this.test = test; } @Override public void run(){ super.run(); //在笔记 (一) 里面提到过,其实觉得可以不加上 test.addId("a"); } } public class ThreadB extends Thread { private Test test; public ThreadB(Test test){ this.test = test; } @Override public void run(){ super.run(); //在笔记 (一) 里面提到过,其实觉得可以不加上 test.addId("b"); } }
同时运行时,Test类中的num变量会被两个线程不同步的修改,出现错误
public class Run{ public static void main(String[] args){ Test test = new Tess(); //!!!!!!!!!!这里很关键,这里是同一个实例对象test!下文会提到! ThreadA athread = new ThreadA(test); athread.start(); ThreadB bthread = new ThreadB(test); bthread.start(); } }
这时,想让他们同步的办法便是给他们的 addId() 方法,加上锁:synchronized 关键字。
synchronized public void addId(String username){ //...中间部分全部相同的代码 }
结论:两个线程访问同一个对象中的同步方法时,一定是线程安全的。
既然有同一个对象中的同步方法,肯定就会有多个对象的情况,这个时候就会有多个对象多个锁的情况:
这里详细说一下synchronized关键字加锁的范围:(本部分加锁范围借鉴了宇学愈多的博文)
- 修饰普通方法(锁住的是当前实例对象)
-
同步代码块传参this(锁住的是当前实例对象)
-
同步代码块传参变量对象 (锁住的是变量对象)
-
同步代码块传参class对象(全局锁)
-
修饰静态方法(全局锁)
构造函数,原型对象,实例对象三者之间的关系
构造函数 ,是一种特殊的方法。主要用来在创建对象时初始化对象, 即为对象成员变量赋初始值,总与new运算符一起使用在创建对象的语句中。特别的一个类可以有多个构造函数 ,可根据其参数个数的不同或参数类型的不同来区分它们 即构造函数的重载。
Example e =
new
Example(n); //构造函数。
通过调用构造函数产生的实例对象,都拥有一个内部属性,指向了原型对象。其实例对象能够访问原型对象上的所有属性和方法。//e 为实例对象
1 修饰普通方法:
public class SynchronizedTest { //锁住了本类的实例对象 public synchronized void test1() { try { logger.info(Thread.currentThread().getName() + " test1 进入了同步方法"); Thread.sleep(5000); logger.info(Thread.currentThread().getName() + " test1 休眠结束"); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) { SynchronizedTest st = new SynchronizedTest(); SynchronizedTest st2 = new SynchronizedTest(); new Thread(() -> { logger.info(Thread.currentThread().getName() + " test 准备进入"); st.test1(); }).start(); new Thread(() -> { logger.info(Thread.currentThread().getName() + " test 准备进入"); st2.test1(); }).start(); } }
本例的实例对象为 st st2 :
- 同一个实例调用会阻塞(开篇提到的例子中,两个线程访问同一个对象实例方法,所以会产生阻塞)
- 不同实例调用不会阻塞
上文的代码的运行结果是没有阻塞的,因为是不同的实例对象,调用了相同的方法 test1() .
2 同步代码块穿参this
- 同一个实例调用会阻塞
- 不同实例调用不会阻塞
public class SynchronizedTest { //锁住了本类的实例对象 public void test2() { synchronized (this) { try { logger.info(Thread.currentThread().getName() + " test2 进入了同步块"); Thread.sleep(5000); logger.info(Thread.currentThread().getName() + " test2 休眠结束"); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) { SynchronizedTest st = new SynchronizedTest(); new Thread(() -> { logger.info(Thread.currentThread().getName() + " test 准备进入"); st.test2(); }).start(); new Thread(() -> { logger.info(Thread.currentThread().getName() + " test 准备进入"); st.test2(); }).start(); } }
和 1 一样,同样是锁住了当前的实例对象
3 同步代码块传参变量对象
- 同一个属性对象才会实现同步
public class SynchronizedTest { public Integer lockObject; public SynchronizedTest(Integer lockObject) { this.lockObject = lockObject; } //锁住了实例中的成员变量 public void test3() { synchronized (lockObject) { try { logger.info(Thread.currentThread().getName() + " test3 进入了同步块"); Thread.sleep(5000); logger.info(Thread.currentThread().getName() + " test3 休眠结束"); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) { SynchronizedTest st = new SynchronizedTest(127); SynchronizedTest st2 = new SynchronizedTest(127); new Thread(() -> { logger.info(Thread.currentThread().getName() + " test 准备进入"); st.test3(); }).start(); new Thread(() -> { logger.info(Thread.currentThread().getName() + " test 准备进入"); st2.test3(); }).start(); } }
同一个实例对象的成员属性肯定是同一个,此处列举的是不同实例的情况,但是 依旧实现了同步,原因如下:
Integer存在静态缓存,范围是-128 ~ 127,当使用Integer A = 127 或者 Integer A = Integer.valueOf(127) 这样的形式,都是从此缓存拿。如果使用 Integer A = new Integer(127),每次都是一个新的对象。此例中,两个对象实例的成员变量 lockObject 其实是同一个对象,因此实现了同步。还有字符串常量池也要注意。所以此处关注是,同步代码块传参的对象是否是同一个。这跟第二个方式其实是同一种。
4、同步代码块传参class对象(全局锁)
所有调用该方法的线程都会实现同步。
public class SynchronizedTest { //全局锁,类是全局唯一的 public void test4() { synchronized (SynchronizedTest.class) { try { logger.info(Thread.currentThread().getName() + " test4 进入了同步块"); Thread.sleep(5000); logger.info(Thread.currentThread().getName() + " test4 休眠结束"); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) { SynchronizedTest st = new SynchronizedTest(); SynchronizedTest st2 = new SynchronizedTest(); new Thread(() -> { logger.info(Thread.currentThread().getName() + " test 准备进入"); st.test4(); }).start(); new Thread(() -> { logger.info(Thread.currentThread().getName() + " test 准备进入"); st2.test4(); }).start(); } }
类锁,直接锁了全局了
5、修饰静态方法(全局锁)
- 所有调用该方法的线程都会实现同步
public class SynchronizedTest { //全局锁,静态方法全局唯一的 public synchronized static void test5() { try { logger.info(Thread.currentThread().getName() + " test5 进入同步方法"); Thread.sleep(5000); logger.info(Thread.currentThread().getName() + " test5 休眠结束"); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) { SynchronizedTest st = new SynchronizedTest(); SynchronizedTest st2 = new SynchronizedTest(); new Thread(() -> { logger.info(Thread.currentThread().getName() + " test 准备进入"); st.test5(); }).start(); new Thread(() -> { logger.info(Thread.currentThread().getName() + " test 准备进入"); st2.test5(); }).start(); new Thread(() -> { logger.info(Thread.currentThread().getName() + " test 准备进入"); SynchronizedTest.test5(); }).start(); } }
结论:synchronized在语法维度上主要分为三个用法
-
静态方法加上关键字
-
实例方法(也就是普通方法)加上关键字
-
方法中使用同步代码块
前两种方式最为偷懒,第三种方式比前两种性能要好。
本篇的最后加上一个多线程的题目:利用5个线程并发执行,num数字累计计数到10000,并打印。
/** * Description: * 利用5个线程并发执行,num数字累加计数到10000,并打印。 * 2019-06-13 * Created with OKevin. */ public class Count { private int num = 0; public static void main(String[] args) throws InterruptedException { Count count = new Count(); Thread thread1 = new Thread(count.new MyThread()); Thread thread2 = new Thread(count.new MyThread()); Thread thread3 = new Thread(count.new MyThread()); Thread thread4 = new Thread(count.new MyThread()); Thread thread5 = new Thread(count.new MyThread()); thread1.start(); thread2.start(); thread3.start(); thread4.start(); thread5.start(); thread1.join(); thread2.join(); thread3.join(); thread4.join(); thread5.join(); System.out.println(count.num); } private synchronized void increse() { for (int i = 0; i < 2000; i++) { num++; } } class MyThread implements Runnable { @Override public void run() { increse(); } } }