Java并发编程(三):多线程安全分析(案例)

目录

 

模拟数组库

Vector与ArrayList

源码对比

Vector

ArrayList + synchronized

Collections工具类

Lock的读锁和写锁

ThreadLocal


模拟数组库

数据库就类似于一个集合,element其实就是相当于数据库中的一行记录。就比如有一个图书表,有人会去查阅、编辑或是添加信息,他们操作的都是一个图书表中的数据;这时候数据库的操作一定是高并发的操作,比如有人在查阅书籍,同时别人在插入图书信息,读操作一定会受到影响。

Book.java

public class Book {
	private String name;
	private double price;
	
	public Book(String name) {
		this.name = name;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public double getPrice() {
		return price;
	}
	public void setPrice(double price) {
		this.price = price;
	}
}

BookTable.java 模拟数据库

import java.util.List;
import java.util.Vector;

public class BookTable {
	private List<Book> bookTable;

	public BookTable() {
		bookTable = new Vector<>(30);
	}
	
	public void add(Book book){
		bookTable.add(book);
	}
	
	public Book get(int index){
		return bookTable.get(index);
	}
	
	public int size(){
		return bookTable.size();
	}
}

 

Vector与ArrayList

Vector与ArrayList两者的底层都是实现了静态数组,而Vector是线程安全的,ArrayList是线程不安全的;

而Vector在1.5之后就被弃用了

源码对比

通过观察Vector源码发现不少方法有synchronized关键字

夸张的是连获取长度的size()方法都上锁了

    public synchronized int size() {
        return elementCount;
    }

接下来我们主要来看一下增删查操作,所有的读和写操作都是用了synchronized对方法进行了锁定

//增
    public synchronized boolean add(E e) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
    }

//查
    public synchronized E get(int index) {
        if (index >= elementCount)
            throw new ArrayIndexOutOfBoundsException(index);

        return elementData(index);
    }

//删
    public synchronized E remove(int index) {
        modCount++;
        if (index >= elementCount)
            throw new ArrayIndexOutOfBoundsException(index);
        E oldValue = elementData(index);

        int numMoved = elementCount - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--elementCount] = null; // Let gc do its work

        return oldValue;
    }

1、比如当一个线程要进行写(add)操作时,它会先获得锁,如果其他线程也要对同一个对象进行写操必须要排队等待

2、如果有两个线程对同一个对象进行操作,一个进行读,一个进行写,因为synchronized只锁代码块,所以读写操作时互不干扰的

 

而ArrayList中的方法没有一个用synchronized修饰!

 

Vector

有两个写线程,一个读线程

public class VectorThread {
	public static void main(String[] args) {
		BookTable bookTable = new BookTable();
		//写a
		new Thread(()->{
			for(int i=0;i<30;i++){
				Book book = new Book("a"+Integer.toString(i));
				bookTable.add(book);
				System.out.println("线程"+Thread.currentThread().getId()+",添加:"+book.getName());
				try {
					Thread.sleep(100);
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}).start();
		
		//写b
		new Thread(()->{
			for(int i=0;i<30;i++){
				Book book = new Book("b"+Integer.toString(i));
				bookTable.add(book);
				System.out.println("线程"+Thread.currentThread().getId()+",添加:"+book.getName());
				try {
					Thread.sleep(100);
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}).start();
			
		//读
		new Thread(()->{ 
			try {
				Thread.sleep(300);
			} catch (Exception e) {
				e.printStackTrace();
			}
			for(int i=0;i<bookTable.size();i++){
				Book book = bookTable.get(i);
				System.out.println("线程"+Thread.currentThread().getId()+",-------读:"+book.getName());
				try {
			 		Thread.sleep(100);
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}).start();
	}
}

由于Vector中的add方法是上了锁的,两个线程对同一个bookTable对象进行写时实际上存在某一线程在add方法外等待阻塞(排队)的情况,这要是在高并发的写操作中是很影响效率的,讲道理多个线程向数据库中插入数据应当是互不干扰的,排队就没什么意义,这也就是Vector的不足之处

。。。。

 

ArrayList + synchronized

改一下BookTable.java中的bookTable变量类型为ArrayList

这时候如果有一个写进程,一个读进程那是互不干扰的

而两个写进程,一个读进程就可能会出现空指针异常,因为多进程同时进入add、get方法时存在modCount等局部变量(共享的);我们需要让写进程结束了再让读进程进来,这时候与Vector不同的是我们应该锁的不是方法而是对象!

public class ArrayListThread {
	public static void main(String[] args) {
		BookTable bookTable = new BookTable();
		//写a
		new Thread(()->{
			synchronized (bookTable) {
				for(int i=0;i<30;i++){
					Book book = new Book("a"+Integer.toString(i));
					bookTable.add(book);
					System.out.println("线程"+Thread.currentThread().getId()+",添加:"+book.getName());
					try {
						Thread.sleep(100);
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
			}
		}).start();
		
		//写b
		new Thread(()->{
			synchronized (bookTable) {
				for(int i=0;i<30;i++){
					Book book = new Book("b"+Integer.toString(i));
					bookTable.add(book);
					System.out.println("线程"+Thread.currentThread().getId()+",添加:"+book.getName());
					try {
						Thread.sleep(100);
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
			}
		}).start();
			
		//读
		new Thread(()->{ 
			try {
				Thread.sleep(300);
			} catch (Exception e) {
				e.printStackTrace();
			}
			synchronized (bookTable) {
				for(int i=0;i<bookTable.size();i++){
					Book book = bookTable.get(i);
					System.out.println("线程"+Thread.currentThread().getId()+",-------读:"+book.getName());
					try {
						Thread.sleep(100);
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
			}
		}).start();
	}
}

结果是先写a,再读a,最后写b;也就是说读也是有排斥性的,这不是我们想要的!

 

Collections工具类

在Collections工具类中提供了不少线程安全的集合

以内部类synchronizedCollection为例

synchronizedCollection源码中提供了一个信号量

final Object mutex;

方法是通过锁对象来实现线程安全的,这个mute就相当于上面例子中的bookTable

    public int size() {
         synchronized (mutex) {return c.size();}
    }

    public boolean add(E e) {
         synchronized (mutex) {return c.add(e);}
    }

    public boolean remove(Object o) {
         synchronized (mutex) {return c.remove(o);}
    }

    ...

也就是说多线程的读写操作都必须排队,一个线程读,其他线程不能读或写。这就导致了锁级别太高了!也不是我们想要的。

 

Lock的读锁和写锁

以上的方法无非是用synchronized关键字锁代码块(Vector)或是锁对象(Collections),但在对数据库读写时都达不到真实效果

Lock 与 synchronized对比: synchronized 时jdk内置的关键字,是jvm级的更为重要;Lock时jdk后期扩展的线程操作的接口,使用lock()获得锁,必须使用unlock()释放锁,而且具有退出和检查机制。

ReentrantLock是一个可重入互斥Lock具有与使用synchronized方法和语句访问的隐式监视锁相同的基本行为和语义,但具有扩展功能。

Lock接口将读写操作分开:ReadLock ,WriteLock;他们都是ReentrantReadWriteLock的静态内部类;而ReentrantReadWriteLock实现了ReadWriteLock

public class ReentrantReadWriteLock
        implements ReadWriteLock, java.io.Serializable {
    public static class ReadLock implements Lock, java.io.Serializable {}
    public static class WriteLock implements Lock, java.io.Serializable {}
}

ReadWriteLock

A ReadWriteLock维护一对关联的locks ,一个用于只读操作,一个用于写入。

  • read lock 允许多线程都在读(对于同一块代码,一个线程加了读锁后,别的线程还可以继续加读锁)
  • write lock 是一个排它锁,与读锁和其他写锁都是排斥的

 

修改一下上面的ArrayListThread.java,有两个读线程和两个写线程

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ArrayListThread {
	public static void main(String[] args) {
		
		BookTable bookTable = new BookTable();
		ReadWriteLock locker = new ReentrantReadWriteLock();
		//写a
		new Thread(()->{
			locker.writeLock().lock();
			for(int i=0;i<30;i++){
				Book book = new Book("a"+Integer.toString(i));
				bookTable.add(book);
				System.out.println("线程"+Thread.currentThread().getId()+",添加:"+book.getName());
				try {
					Thread.sleep(100);
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
			locker.writeLock().unlock();
		}).start();
		
		//写b
		new Thread(()->{
			locker.writeLock().lock();
			for(int i=0;i<30;i++){
				Book book = new Book("b"+Integer.toString(i));
				bookTable.add(book);
				System.out.println("线程"+Thread.currentThread().getId()+",添加:"+book.getName());
				try {
					Thread.sleep(100);
				} catch (Exception e) {
					e.printStackTrace();
				}
				}
			locker.writeLock().unlock();
		}).start();
			
		//读a
		new Thread(()->{ 
			try {
				Thread.sleep(300);
			} catch (Exception e) {
				e.printStackTrace();
			}
			locker.readLock().lock();;
			for(int i=0;i<bookTable.size();i++){
				Book book = bookTable.get(i);
				System.out.println("线程"+Thread.currentThread().getId()+",-------读:"+book.getName());
				try {
					Thread.sleep(100);
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
			locker.readLock().unlock();
		}).start();
		
		//读b
		new Thread(()->{ 
			try {
				Thread.sleep(300);
			} catch (Exception e) {
				e.printStackTrace();
			}
			locker.readLock().lock();;
			for(int i=0;i<bookTable.size();i++){
				Book book = bookTable.get(i);
				System.out.println("线程"+Thread.currentThread().getId()+",-------读:"+book.getName());
				try {
					Thread.sleep(100);
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
			locker.readLock().unlock();
		}).start();
		
	}
}

结果:先写a,再写b,然后两个读线程是同时交替地读

总结:

  1. 如果将上面四个线程的顺序改为写读写读,结果是相同的,这是应为WriteLock的优先级高于ReadLock;
  2. WriteLock与WriteLock和ReadLock都是有排斥性的,只能顺序执行;
  3. ReadLock是可以并发读取的;
  4. 想要达到顺序执行的效果,通常是读锁写锁配合使用,读操作时可以共享的,写操作时不能共享的

 

ThreadLocal

提供线程局部变量。 这些变量与其正常的对应方式不同,因为访问一个的每个线程(通过其get或set方法)都有自己独立初始化的变量副本。 ThreadLocal实例通常是希望将状态与线程关联的类中的私有静态字段(例如,用户ID或事务ID)。 

比如网站上用户操作向数据库中存储数据,那么每个人的每次请求都有自己的Connection,这些局部变量都存储在ThreadLocal中,所有线程之间互不干扰。

注意实例化时一定要作为静态变量对象;get()从静态对象中把当前线程存储的S对象取出来;set()使用当前线程信息,存储S为自己的局部变量。

比如一共有10个线程,我们向10个线程中分别存放不同的局部变量:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

public class ThreadId {
    /* AtomicInteger:在高并发的环境下,int值可以进行原子性的变化(采用CAS算法)
     * int getAndIncrement() :原子上+1当前值
     * int getAndDecreament() :原子上-1当前值
     */

    private static final AtomicInteger nextId = new AtomicInteger(0);  	//初始值为0

    private static final ThreadLocal<Integer> threadId =new ThreadLocal<Integer>();

    public static int get() {
        return threadId.get();
    }
    
    public static void set(Integer i){
    	threadId.set(i);
    }
    
    public static void main(String[] args) {
		ExecutorService pool = Executors.newCachedThreadPool();
		for(int i=0;i<10;i++){
			pool.execute(new Runnable() {
				public void run() {
					ThreadId.set(nextId.getAndIncrement());
					int a = ThreadId.get();
					System.out.println("线程"+Thread.currentThread().getId()+",局部变量a="+a);
				}
			});
		}
		pool.shutdown();
	}
} 

结果如下

 

 


版权声明:本文为qq_39192827原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。