Java并发编程实战读书笔记(1)线程安全性

对象状态

在了解线程安全之前先了解一下什么是对象状态。从非正式的意义上来说,对象的状态是指存储在状态变量(例如实例或静态域)中的数据。对象的状态可能包括其他依赖对象的域。例如,某HashMap的状态不仅存储在其对象本身,还存储在许多Map.Entry对象中。在对象的状态中包含了任何可能影响其外部可见行为的数据。

1.什么是线程安全

在这之前先看一段线程不安全的代码

public class Test {
     private  static  int count;
    private static class Thread1 extends Thread {
        public void run() {
            for (int i = 0; i < 1000; i++) {
                count ++;
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

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

这段代码实现的逻辑很简单,首先定义了一个int型的count变量,然后开启了两个线程,每个线程执行1000次循环,循环中对count进行加1操作。等待两个线程都执行完成后,打印count的值。那么这段代码的输出结果是多少呢?可能很多人会说是2000。但是程序运行后却发现结果大概率不是2000,而是一个比2000略小的数,比如1998这样,而且每次运行的结果可能都不相同。这就是线程不安全。为什么是不安全的呢?因为count++的指令在实际执行的过程中不是原子性的,而是要分为读、改、写三步来进行;即先从内存中读出count的值,然后执行+1操作,再将结果写回内存中。

当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或者协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
上述说法可能有点绕,简单来说就是一个类或者一段代码无论是在单线程还是多线程下执行结果都是一致,正确的,那么就是线程安全的。

一个简单的线程安全的类

public class Test implements Servlet{
         public void service(ServletRequest req, ServletResponse reps) throws ServletException, IOException {
                BigInteger i = extractFromRequest(req);
                BigInteger[] factors=factor(i);
                encodeIntResponse(resp,factors);
            }
}

上述Test类就是一个简单的线程安全的类,因为他是无状态的:它既不包含任何域,也不包含任何对其他类中域的引用。计算过程中的临时状态仅存在于线程栈上的局部变量中,并且只能由正在执行的线程访问(参考JVM内存分配结构)。

总结:
1.线程安全是指在多线程环境下,程序可以始终执行正确的行为,符合预期的逻辑
2.无状态对象一定是线程安全的

2.原子性

何为原子性:原子性在一个操作是不可中断的,要么全部执行成功要么全部执行失败

public class Test {
     private  static  int count;
    private static class Thread1 extends Thread {
        public void run() {
            for (int i = 0; i < 1000; i++) {
                count ++;
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

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

我们再次回到上述示例代码来 count++看似是一个操作,其实不然。要分为读、改、写三步来进行;即先从内存中读出count的值,然后执行+1操作,再将结果写回内存中。大当两个线程在没有同步的情况下同时执行递增操作的时候,如果count的初始值为9,那么在某些情况下,每个线程读到的值都为9,并且都将其值设为10。显然这并不是我们期望看到的情况。在并发编程中,这种由于不恰当的执行时序而出现不正确的结果有一个正式的名字叫做:竞态条件

2.1竞态条件

定义:由于不恰当的执行执行时序而出现不正确的执行结果
当某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生竞态条件。
除此之外最常见的竞态条件就是“先检查后执行”:通过一个可能失效的观测结果来决定下一步的动作。而其中最常见的情况就是延迟初始化。

   public class LazyInitRace {

        private User instace = null;

        public User getInstance() {

            if (instace == null) {
                instace = new User;
            }
            return instace;
        }
    }

上述例子中将User对象的初始化延迟到调用getInstance()时才进行,同时还要确保只被初始化一次。然而如果当两个线程A和B同时执行了getInstance()。当A看到instance为空,因而创建一个新的User实例。B同样需要判断instance是否为空。然后此时instance是否为空,要取决于不可预测的时序,包括线程的调度方式。很有可能会出现A,B同时执行到了if(instace ==null) 继而A,B分别创建了一个新的User实例。这与我们的设计初衷不相符合。在多线程也并非是线程安全的。

我们将上述的递增操作count++中的“读取-修改-写入”以及“先检查后执行”等统称为复合操作。如果我们让复合操作也是原子性的,即操作不可分割,那么也能保证线程安全。换个说话即时在某个线程修改修改该变量时,通过某种方式防止其他线程使用这个变量,从而确保其他线程只能在修改操作完成之前或者之后读取和修改状态,而不是在修改状态的过程中。
除此之外我们也可以使用一些原子变量类用于实现在数值和对象引用上的原子状态转换。

  public class Test extends Thread{
        private  static  AtomicLong count = new AtomicLong(0);

        public void run() {
            for (int i = 0; i < 1000; i++) {
               count.incrementAndGet();
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        }

通过使用AtomicLong来代替long类型的计数器,能够确保所有计数器状态的访问操作都是原子的。

3.加锁机制

3.1内置锁

除了上述中提到的使用原子操作来确保线程安全,Java中还提供了一种内置的锁机制来支持原子性:同步代码块(Synchronized Block)。
同步代码块包含两部分:一个作为锁的对象引用,一个作为由这个锁保护的代码块。

synchronized (lock){
//访问或修改由锁保护的共享状态
}

与此同时还有以synchronized来修饰方法的同步方法,其就是一种横跨整个方法体的同步代码块,其中该同步方法的锁就是方法调用所在的对象。静态方法以Class对象为锁。

public synchronized void test(){

}

每个Java对象都可以当做一个实现同步的锁,这些锁被称为内置锁或者监视器锁。线程在进入同步代码块之前会自动获的锁,并且在退出同步代码块时自动释放锁。
Java的内置锁相当于一种互斥体(互斥锁),这意味着最多只有一个线程能够持有这种锁。当线程A尝试获取一个由线程B持有的锁时,线程A必须等待或者阻塞,直到线程B释放这个锁为止。如果B永远不释放锁,那么A也将永远等下去。由于每次只有由一个线程执行内置锁保护的代码,因此这个锁保护的同步代码块会以原子方式执行。

3.2重入

当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。然而,由于内置锁是可重入的,因此如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。“重入”意味着获取锁的操作的粒度是“线程”而不是“调用”。
重入的一种实现方法是,为每个锁关联一个获取计数值和一个所有者的线程。当计数值为0时,这个锁就被认为是没有被任何线程持有。当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且将获取计数器值置为1。如果同一个线程再次获取这个锁,计数值将递增,而当线程退出同步代码块时,计数器会相应的递减。当计数值为0时,这个锁将被释放。

public  class Widget {

        public synchronized void doSomething() {
      //do something
        }
    }

public  class LoggingWidget extends Widget {
        public synchronized void doSomething() {
            //do something
            super.doSomething();
        }
    }

上述代码中,子类改写了父类中的synchronized方法,然后调用父类中的方法,此时如果没有可重入的锁,那么这段代码将产生死锁。由于Widget和LoggingWidget中doSomething方法都是synchronied方法,因此每个doSomething方法在执行之前都会获取Widget上的锁,那么在调用super.doSomething时将无法获得Widget上的锁。可重入锁避免了这种死锁情况的发生。

4.用锁来保护状态

未完待续。

你可能感兴趣的:(并发编程)