3.对象以及变量的并发访问

对象以及变量的并发访问

1.synchroized同步方法

    多个线程操作一个对象,那么该线程内的私有变量,如果没有特殊的处理,那么可能就是非线程安全的

    方法内的变量为线程安全

    多余synchronized修饰的方法,操作的是当前对象的锁

class HasPrivateNum{
private int num = 0;
synchronized public void addNum(String userName) throws InterruptedException {
Thread.sleep(10000);
if(userName.equals("a")){
num = 100;
System.out.println("a set over!");
}else{
num = 200;
System.out.println("b set over!");
}
System.out.println(userName + " num = " + num);

}
public void addNumAsc(String userName){
num += 1;
System.out.println(userName + " num = " + num);
}
}

     上述代码中,如果该对象给其他的两个线程操作,那么只有在两个线程同时操作addNum的时候才具有同步性,只有增加了同步块的方法才具有锁性质 addNumAsc是没有同步功能的, addNumAsc也可以在同步的时候对属性进行操作,可能会造成脏读的情况

2.synchroized锁重入

    synchronize在执行多个有锁的方法时候,他同样可以得到该锁

    synchronized public void service1(){ service2();}

    synchronized public void service2(){service3();}

    synchronized public void service3(){System.out.println(“执行3”)}

    当他执行的时候是永远可以得到锁的

    这里引发了一些问题:加入A线程B线程同时调用synchronize修饰的两个方法 A先执行(即A先拿到线程锁) 那么B会在A执行完毕之后才可以执行

    结论:在两个同步代码块修饰的方法中 永远是先拿到对象锁的执行完毕 倘若B线程是调用非同步方法 那么B不需要等到A释放锁。 倘若B执行的普通方法之中有调用到任何同步的方法 那么一定需要等到A释放锁之后才可以执行

    

3.出现异常 则锁自动释放

4.同步不具有继承性

5.同步代码块:

    method(){

        执行步骤A

        synchronized(this){

            执行内容

        }

        执行步骤B

    }

     如果两个线程同时执行method, 那么在执行A的时候是先异步执行(无序),在先拿到锁的对象执行完代码块之后,后续线程才会执行代码块, 但是倘若执行代码块之间的时间很短 那么执行步骤B 也会是无序的

从上述例子中可以看出,所有的synchronized修饰的代码,都是统一的锁 无论是不同的方法之间的先后调用以及同步代码块之间的调用   统一来说,即为这个对象的锁

 

class HasSynField{
private String field = new String();
  public void printlnName( ) throws InterruptedException {
     synchronized (field){
   System.out.println("aa --begin");
   Thread.sleep(3000);
   System.out.println("aa --end");
  }
  }
    synchronized public void printlnField() throws InterruptedException {
System.out.println("bb --begin");
Thread.sleep(3000);
System.out.println("bb --begin");
}
    public void printlnName2() throws InterruptedException {
synchronized (field){
System.out.println("cc --begin");
Thread.sleep(3000);
System.out.println("cc --END");
}
}

public void printName3(){
this.field = "aaa"; //此处是异步的 你要特别注意 所谓对象锁 只是针对同步包裹起来的部分 其他部分是异步的 就像同步方法一样 不加修饰的 则是异步
}
}

 此处一个是同步field这个属性,另外一个是同步当前对象 因此 这个地方如果两个线程分别执行该实例的不同方法 则是异步的,因此使用synchronized(this)和synchronized(非this)是异步的,但是这里的printlnName和printlnName2是同步的 因为他是一个对象锁

 

6.同步静态方法:给class上锁

    synchronized public static void println() throws InterruptedException {
System.out.println("bb --begin");
Thread.sleep(3000);
System.out.println("bb --END");
}
    synchronized public static void println2() throws InterruptedException {
System.out.println("bb --begin");
Thread.sleep(3000);
System.out.println("bb --END");
}

当他是如此修饰的时候,与同步非静态方法以及同步某个属性都是加在不同的锁 因此是异步的,但是上面两种是同步的
还有当同一类的两个实例去调用静态方法的时候也会是同步的 两个线程操作两个实例对象 当他们调用的是静态方法的时候 如果被加锁了 则是同步的。不过大多数的时候 调用静态方法都不会使用实例调用 而是通过类直接调用 (这里就涉及了java怪异的地方)

7.volatile关键字 使得线程总是从内存中读取数据 而不是缓存(针对服务器环境)

public class VolatileThread {
public static void main(String[] args) {
PrintString printString = new PrintString();
new Thread(printString).start();
System.out.println("需要停止");
printString.setContinuePrint(false);

}
}

class PrintString implements Runnable{
private boolean isContinuePrint = true;
public void printStringMethod(){
try{
while(isContinuePrint == true){
System.out.println("run println threadName" + Thread.currentThread().getName());
Thread.sleep(1000);
}
}catch (Exception e){
e.printStackTrace();
}
}
public void setContinuePrint(boolean continuePrint) {
isContinuePrint = continuePrint;
}
@Override
public void run() {
printStringMethod();
}
}
这一段代码结果如我们预料,在一个main方法中运行的时候是没有问题的 当他再服务器中运行的时候 就会出现死循环 因为在服务器中是分公共堆栈和私有堆栈的,在服务器中才会有不同步的问题 当前线程的变量一般读取都在私有堆栈中 如果是共享变量可能就会有此问题 因而需要volatile
volatile private boolean isContinuePrint = true;

当然写两个同步代码块加锁给这个变量也是可以实现同步的

比较:
a.volatile是线程同步的轻量级实现 性能比synchronized要好 volatile只能修饰变量 而synchronized可以修改方法以及代码块,对象
b.volatile不会阻塞
c。volatile能保证数据的可见性,但是不能保证数据的原子性,而synchronized可以保证原子性和可见性,因为他会将私有内存和公有内存中的数据同步
因此volatile是为了保证数据的可见性,synchronized是为了解决多个线程之间资源的同步性

8.volatile的非原子性

    问题来了,既然它可以保证修改的值立即能更新到主存,其他线程也会捕捉到被修改后的值,那么为什么不能保证原子性呢?
    首先需要了解的是,Java中只有对基本类型变量的赋值和读取是原子操作,如i = 1的赋值操作,但是像j = i或者i++这样的操作都不是原子操作,因为他们都进行了多次原子操作,比如先读取i的值,再将i的值赋值给j,两个原子操作加起来就不是原子操作了。

    所以,如果一个变量被volatile修饰了,那么肯定可以保证每次读取这个变量值的时候得到的值是最新的,但是一旦需要对变量进行自增这样的非原子操作,就不会保证这个变量的原子性了。

    例如

    一个变量i被volatile修饰,两个线程想对这个变量修改,都对其进行自增操作也就是i++,i++的过程可以分为三步,首先获取i的值,其次对i的值进行加1,最后将得到的新值写会到缓存中。当这三个步骤被多个线程处理时候 就会出现交叉的情况
    线程A首先得到了i的初始值100,但是还没来得及修改,就阻塞了,这时线程B开始了,它也得到了i的值,由于i的值未被修改,即使是被volatile修饰,主存的变量还没变化,那么线程B得到的值也是100,之后对其进行加1操作,得到101后,将新值写入到缓存中,再刷入主存中。根据可见性     的原则,这个主存的值可以被其他线程可见。
   问题来了,线程A已经读取到了i的值为100,也就是说读取的这个原子操作已经结束了,所以这个可见性来的有点晚,线程A阻塞结束后,继续将100这个值加1,得到101,再将值写到缓存,最后刷入主存,所以即便是volatile具有可见性,也不能保证对它修饰的变量具有原子性。

即在一个线程中多次改变该值的话 则不在具有原子性了,可见性在于查看他的值 原子性是改变他的值  












 

posted @ 2020-12-20 17:41  发条良子  阅读(105)  评论(0)    收藏  举报