Java并发基础<2>

Volatile 变量的声明


11/21/2016 7:21:53 PM From 《高并发程序设计》

volatile 保证一个线程修改变量之后,另一个线程能看到这个改动。

  声明一个volatile变量相当于告诉虚拟机,这个变量的值极有可能被修改,为了确保该变量能被应用程序的所有线程访问看到,需要用volatile去声明,在本人所学的Linux C中,定义的volatile变量意味着不对这个变量去进行编译优化,即每次都是从内存中读取该数据,而不是从被优化存储到cache或者寄存器中读取。我想意思大概是相近的吧,也许就是一个意思呢,不知道。

 /**
 * Created by loveqh on 2016/11/19.
 */
public class JoinMain {
    public volatile static int i = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                for (i = 0; i < 100000; i++) ;
            }
        });
        t.start();
        t.join();
        System.out.println(i);
    }
}

输出为

10000

  虽然volatile对于原子性起了很大的帮助,但是,但是,volatile不能代替锁,无法保证一些复合操作的原子性,如 i++ 如下 因为2个线程同时写入i时,一个线程的结果会覆盖另外一个线程的结果。

package ParallelBasic;

/**
 * 文件描述:
 * 作者: bamboo
 * 时间: 2016/11/21
 */
public class MultiThreadLong {
    public volatile static long i = 0; //volatile 不能代替锁
    public static class PlusTask implements Runnable {

        @Override
        public void run() {
            for (int k = 0; k < 100000; k++) {
                i++;
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(new PlusTask());
            threads[i].start();
        }

        for (int i = 0; i < 10; i++) {
            threads[i].join();
        }
        System.out.println(i);
    }
}

  输出935458。因为2个线程同时写入i时,一个线程的结果会覆盖另外一个线程的结果。


  来看另一个的例子:


package ParallelBasic;

/**
 * 文件描述:
 * 作者: bamboo
 * 时间: 2016/11/21
 */
public class MultiThreadLong {

    private static boolean ready;
    private static int number;

    private static class ReaderThread implements Runnable {


        @Override
        public void run() {
            while (!ready) ;
            System.out.println(number);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new ReaderThread());
        t1.start();
        Thread.sleep(1000);
        number = 42;
        ready = true;
        Thread.sleep(1000);
    }
}

  如果就这样执行,那么该程序永远不会停止,原因是在Server模式下(常用的普通模式 通过java -version可查看) 系统对代码进行了优化(具体就是发现ready变量一直使用,将其放在cache或者寄存器中)所有ready永远不会为false。正确的做法是在ready定义处添加volatile修饰。
在此处提到Server模式。顺便说一下:

  • 虚拟机的Server模式,启动较慢(10%),但是一旦运行性能提升明显,适合做服务器。

  • 虚拟机的Client模式,适合GUI界面的交互应用。

  • JVM在client模式默认-Xms是1M,-Xmx是64M;JVM在Server模式默认-Xms是128M,-Xmx是1024M

  • -Xms指的是初始堆大小 -Xmx指的是最大堆大小.


ThreadGroup 线程组


  线程组就是将相同职能的线程放在一个组里,利于分类和管理。

package ParallelBasic;

/**
 * 文件描述:
 * 作者: bamboo
 * 时间: 2016/11/21
 */
public class ThreadGroup implements Runnable {

    @Override
    public void run() {
        String groupAndName = Thread.currentThread().getThreadGroup().getName() +
                "_" + Thread.currentThread().getName();
        while (true) {
            System.out.println("I am" + groupAndName);
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        ThreadGroup tg = new ThreadGroup("PrintGroup");
        Thread t1 = new Thread(tg, new ThreadGroupName(), "T1");//"T1" the name of the new thread
        Thread t2 = new Thread(tg, new ThreadGroupName(), "T2");
        t1.start();
        t2.start();
        System.out.println(tg.activeCount());//tg.activeCount() 由于线程的动态的,所以这个值是估计值
        tg.list();//打印线程组
    }
}

输出

2
I am PrintGroup_T1
java.lang.ThreadGroup[name=PrintGroup,maxpri=10]
    Thread[T1,5,PrintGroup]
    Thread[T2,5,PrintGroup]
I am PrintGroup_T2
I am PrintGroup_T2
I am PrintGroup_T1
I am PrintGroup_T1
I am PrintGroup_T2 ....

  值得注意的是:tg.stop()方法, 会停止所有线程组中的线程,很暴力 和Thread.sleep()方法一样,所以现在被废弃了。


Daemon 守护线程


守护线程是系统的守护者,运行在后台的线程。当用户线程全部结束时,守护线程无事可做自然退出。

package ParallelBasic;

/**
 * 文件描述:守护线程如垃圾回收线程 JIT线程
 * 当用户线程结束时,只有守护线程时,无事可做,就会自然退出
 * 作者: bamboo
 * 时间: 2016/11/21
 */
public class Daemon {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("I am alive");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t1.setDaemon(true);
        t1.start();
        Thread.sleep(3000);
    }
}

值得注意的是

        t1.setDaemon(true);
        t1.start();

  应该先设置守护线程,再start,如果设置错了,会抛出设置守护线程错误异常,但是程序依然运行下去,原因是被当做用户线程继续执行了。当主线程结束时,守护线程也消失了。


Priority 线程优先级


java中线程优先级1-10 10为最高 和Linux值相反,linux优先级1最大

  • MIN_PRIORITY=1;
  • MAX_PRIORITY=10;
  • NORM_PRIORITY=5;

`

package ParallelBasic;

/**
 * 文件描述:
 * java中线程优先级1-10 10为最高  和Linux值相反,linux优先级1最大
 * MIN_PRIORITY=1;
 * MAX_PRIORITY=10;
 * NORM_PRIORITY=5;
 * 作者: bamboo
 * 时间: 2016/11/21
 */
public class ThreadPriority {
    public static class HighPriority implements Runnable {

        static int count = 0;

        @Override
        public void run() {
            while (true) {
                synchronized (ThreadPriority.class) {
                    count++;
                    if (count > 100000000) {
                        System.out.println("HighPriority is complete");
                        break;
                    }
                }
            }
        }
    }

    public static class LowPriority implements Runnable {

        static int count = 0;

        @Override
        public void run() {
            while (true) {
                //synchronized (ThreadPriority.class) 产生一次资源竞争,让效果更明显
                synchronized (ThreadPriority.class) {
                    count++;
                    if (count > 100000000) {
                        System.out.println("LowPriority is complete");
                        break;
                    }
                }
            }
        }
    }

    public static void main(String[] args) {
        Thread h = new Thread(new HighPriority());
        Thread l = new Thread(new LowPriority());
        h.setPriority(Thread.MAX_PRIORITY);
        l.setPriority(Thread.MIN_PRIORITY);
        l.start();
        h.start();
    }

}

输出的结果是

HighPriority is complete
LowPriority is complete
Process finished with exit code 0

  虽然体现出的结果是这样,但是高优先级只是在大部分情况下会优先于低优先级线程执行,不一定每次都是这样。使用了synchronized (ThreadPriority.class) 产生一次资源竞争,让效果更明显。


synchronized 同步关键字


实现线程间的同步,工作就是对同步代码加锁。

  一次只能有一个线程进入到代码块中,从而保证线程的安全性。即当线程A写入的时候,线程B不仅不能写,还不能读(因为进入不到代码块中)。

synchronized的多种用法

  • 指定加锁对象:对给定对象加锁,进入同步代码块前要获得给定对象的锁。
  • 直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获取当前实例的锁。
  • 直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获取当前类的锁。

下列代码用来解决volatile复合操作原子性问题的。

package ParallelBasic;

/**
 * 文件描述:
 * 作者: bamboo
 * 时间: 2016/11/21
 */
public class Synchronized {
    public static long i = 0; //volatile 不能代替锁
    static PlusTask plusTask= new PlusTask();
    public static class PlusTask implements Runnable {

        @Override
        public void run() {
            for (int k = 0; k < 10000; k++) {
                increase();
            }
        }

        synchronized void increase() {
            i++;
        }

        public static void main(String[] args) throws InterruptedException {
            Thread[] threads = new Thread[10];
            for (int i = 0; i < 10; i++) {
                threads[i] = new Thread(new PlusTask());
                threads[i].start();
            }

            for (int i = 0; i < 10; i++) {
                threads[i].join();
            }
            System.out.println(i);
        }
    }
}

结果却输出了

95262

注意到上述使用synchronized的方法,分析上述代码得知,synchronized void increase(){}

是直接作用于实例方法的,而在main函数中,new了非常10个实例,说明加锁都加到了不同的对象上,这样就会造成, 2个线程同时写入i时,一个线程的结果会覆盖另外一个线程的结果。

正确的做法是有

  • 在main函数中,使用同一个对象创建线程。

  • 将increase方法提升为static类方法,这样每次都锁定类。

  • 如果i不是数据类型,而是一个对象的话,那么可以直接synchronized(Object instance) 此处不受用。

并行程序中隐藏的Bug 虽然小,但是容易致命


  • 溢出

  • 并发下的ArrayList

  • 并发下的HashMap

  • 加锁对象的错误


溢出,无法提示的错误


 /**
 * 文件描述:
 * 作者: bamboo
 * 时间: 2016/11/21
 */
public class OutIndex {
    public static void main(String[] args) {
        int a =1023454656;
        int b =1232234234;
        System.out.println(a+b);
    }
}


out:
-2039278406

Process finished with exit code 0

  如果是单单这样的一个程序当然会想到,是溢出的问题,如果是在很复杂的环境中,出现了这样的错误,那种调不出来的bug,真是令人难受。


并发下的ArrayList


并发下的ArrayList,非线程安全。

package ParallelBasic;

import java.util.ArrayList;
import java.util.Vector;

/**
 * 文件描述:线程不安全容器,多线程下容易出错
 * 作者: bamboo
 * 时间: 2016/11/21
 */
public class ArrayListMultiThread {
    static ArrayList<Integer> al = new ArrayList<Integer>(10);
//    static Vector<Integer> al = new Vector<>(10);

    public static class AddThread implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 100000; i++) {
                al.add(i);
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new AddThread());
        Thread t2 = new Thread(new AddThread());
        t1.start();t2.start();
        t1.join();
        t2.join();
        System.out.println(al.size());
    }
}

输出为

Exception in thread "Thread-0" java.lang.ArrayIndexOutOfBoundsException: 549
at java.util.ArrayList.add(ArrayList.java:441)
at ParallelBasic.ArrayListMultiThread$AddThread.run(ArrayListMultiThread.java:28)
at java.lang.Thread.run(Thread.java:744)

或者:

100430

  原因是ArrayList在扩容过程中,内部的一致性被破坏,但由于没有锁保护,另一个线程访问到了不一致的内部状态,导致越界。

  也可能直接打印一个小于如178233 这样的 值,但是不会报错,两个线程访问了同一位置导致覆盖。

解决办法用vector代替ArrayList

static Vector<Integer> al = new Vector<>(10);


并发下的HashMap

并发下的HashMap,非线程安全。

下列代码有毒

package ParallelBasic;

import java.util.HashMap;
import java.util.Map;

/**
 * 文件描述:并发下的hashMap也不是线程安全的
 * 出现死循环,用ConcurrentHashMap
 * 作者: bamboo
 * 时间: 2016/11/21
 */
public class HashMapMultiThread {
    static Map<String, String> map = new HashMap<>();

    public static class AddThread implements Runnable {
        int start = 0;

        public AddThread(int start) {
            this.start = start;
        }

        @Override
        public void run() {
            for (int i = start; i < 100000; i += 2) {
                map.put(Integer.toString(i), Integer.toBinaryString(i));
            }
        }

        public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(new AddThread(0));
            Thread t2 = new Thread(new AddThread(1));
            t1.start();t2.start();
            t1.join();
            t2.join();
            System.out.println(map.size());
        }
    }
}

  当我运行代码的时候,机子卡主了,吓得我赶紧退出程序,然后将循环数值改小一点,发现CPU占用率高的飞起,马上就99%了。然后再改小一点,发现ok了。


于是我使用jps命令查看线程运行状态。

C:\Users\bamboo>jps
8816 Launcher
8232 AppMain
7684 Jps
7936


C:\Users\bamboo>jstack 8232


....

"Thread-1" prio=6 tid=0x000000000a0bf000 nid=0x1a80 runnable [0x000000000a8ae000]
   java.lang.Thread.State: RUNNABLE
        at java.util.HashMap.put(HashMap.java:498)
        at ParallelBasic.HashMapMultiThread$AddThread.run(HashMapMultiThread.java:25)
        at java.lang.Thread.run(Thread.java:744)

"Thread-0" prio=6 tid=0x000000000a0be800 nid=0x249c runnable [0x000000000ab4e000]
   java.lang.Thread.State: RUNNABLE
        at java.util.HashMap.put(HashMap.java:498)
        at ParallelBasic.HashMapMultiThread$AddThread.run(HashMapMultiThread.java:25)
        at java.lang.Thread.run(Thread.java:744)

...

"main" prio=6 tid=0x000000000246f000 nid=0x63c in Object.wait() [0x000000000267e000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x00000000eb151580> (a java.lang.Thread)
        at java.lang.Thread.join(Thread.java:1280)
        - locked <0x00000000eb151580> (a java.lang.Thread)
        at java.lang.Thread.join(Thread.java:1354)
        at ParallelBasic.HashMapMultiThread$AddThread.main(HashMapMultiThread.java:33)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:606)
        at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)

....

  注意到HaspMap.put函数的498行似乎发生了什么,于是我就跑去看了,下列第一行就为498行。

   for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

  看起来就是如同链表遍历一样,一个一个往下,但是由于在多线程环境下,可能发生冲突,链表结构被破坏,形成一个环,这样下去上述的迭代过程是一个死循环。 最简单的就是key1和key2 相互指向对方。
在JDK1.8已经做了大改动,很大程度上解决这个问题,但是还是有问题,最好的方法就是使用ConcurrentHashMap


加锁对象的错误


下列就是一个加锁加错对象的例子

package ParallelBasic;

public class LockInteger implements Runnable {
    public static Integer i = 0;
    static BadLockOnInteger instance = new BadLockOnInteger();
    @Override
    public void run() {
        for (int j = 0; j < 1000000; j++) {
            synchronized (i) {
                i++; //对于此处,就是新建一个Integer对象,并将它的引用赋值给i;
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1= new Thread(instance);
        Thread t2= new Thread(instance);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}


    输出:
    1883900

    Process finished with exit code 0

  为什么呢,讲道理,该加锁的加锁了。问题出现在这个Integer上。Integer这个类的对象是不可变对象。对其进行i++时,就是新建一个Integer对象,并将它的引用赋值给i,意思就是可能将锁加到了不同的Integer对象上,导致了2个线程同时写入i时,一个线程的结果会覆盖另外一个线程的结果。证明一下:
- 将经过编译的class文件使用javap反编译一下,并找到run方法

C:\Users\bamboo\Desktop\单源\Leetcode\out\ParallelBasic>javap -c -public BadLockOnInteger.class
Compiled from "BadLockOnInteger.java"
public class ParallelBasic.BadLockOnInteger implements java.lang.Runnable {
  public static java.lang.Integer i;

  public ParallelBasic.BadLockOnInteger();

    .....
  public void run();
    Code:
       0: iconst_0
       1: istore_1
       2: iload_1
       3: ldc           #2                  // int 1000000
       5: if_icmpge     55
       8: getstatic     #3                  // Field i:Ljava/lang/Integer;
      11: dup
      12: astore_2
      13: monitorenter
      14: getstatic     #3                  // Field i:Ljava/lang/Integer;
      17: astore_3
      18: getstatic     #3                  // Field i:Ljava/lang/Integer;
      21: invokevirtual #4                  // Method java/lang/Integer.intValue:()I
      24: iconst_1
      25: iadd
      26: invokestatic  #5                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      29: dup
      30: putstatic     #3                  // Field i:Ljava/lang/Integer;
      33: astore        4
      35: aload_3
      36: pop
      37: aload_2
      38: monitorexit
      39: goto          49
      42: astore        5
      44: aload_2
      45: monitorexit
      46: aload         5
      48: athrow
      49: iinc          1, 1
      52: goto          2
      55: return
    Exception table:
       from    to  target type
          14    39    42   any
          42    46    42   any

 ...
}

  第26行出现了感兴趣的东西,于是就去找到了Integer.valueOf(Int i)方法:
public static Integer valueOf(int i) {
assert IntegerCache.high >= 127;
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}

  果然,不可变对象原来是这样的。又学到了,这让我想到了另一个String和StringBuffer的区别,大体上也是这样的吧。很关键。

上述代码的修改方法就是将

synchronized (i);

改为

synchronized (instance);
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值