[Java]并发编程-API

一、synchronized和volatile关键字

手动上锁,自动释放锁。

锁的是对象有两种:1、类的实例,指向堆里的唯一的实例;2、类的字节码。

锁对象的改变

package com.sync;

import java.util.concurrent.TimeUnit;

public class ChangeOfLockObject {
    Object o = new Object();

    public void test(){
        synchronized (o) {
            while (true) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName());
            }
        }
    }

    public static void main(String[] args) {
        ChangeOfLockObject demo = new ChangeOfLockObject();

        new Thread(demo :: test, "t1").start();

        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Thread t2 = new Thread(demo :: test, "t2");

        demo.o = new Object();
        //t2能否执行?
        t2.start();
    }
}

Sync锁的是一个对象,但是如果实例变了,证明这个锁变了,证明以上程序用的不是同一把锁

2、不要以字符串常亮作为锁的对象

public class constantObject{
  String s1 = "hello";
    String s2 = "hello";

    public void test1(){
        synchronized (s1) {
            System.out.println("t1 start...");
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t1 end...");
        }
    }

    public void test2(){
        synchronized (s2) {
            System.out.println("t2 start...");
        }
    }

    public static void main(String[] args) {
        constantObject demo = new constantObject();
        new Thread(demo :: test1,"test1").start();
        new Thread(demo :: test2,"test2").start();
    }
}

以上的test1和test2其实锁定的事同一个对象。使用new String()可以。

3、同步块代码中语句越少越好

public class SynchronizationBlockCode  {

    int count = 0;

    public synchronized void test1(){
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        count ++;

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void test2(){
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        synchronized (this) {
            count ++;
        }

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

业务逻辑中只有count++这句需要sync,这时不应该给整个方法上锁。采用细粒度的锁,可以使线程争用时间变短,从而提高效率

4、同步方法与非同步方法是否可以同时调用?

public class Demo{

    public synchronized void test1(){
        System.out.println(Thread.currentThread().getName() + " test1 start...");
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " test1 end...");
    }

    public void test2(){
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " test2");
    }

    public static void main(String[] args) {
        Demo demo = new Demo();
        new Thread(demo :: test1,"test1").start();
        new Thread(demo :: test2,"test2").start();
    }

}

可以同时调用

5、脏读问题

public class Demo {

    String name;
    double balance;

    public synchronized void set(String name,double balance){
        this.name = name;
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.balance = balance;
    }

    public /*synchronized*/ double getBalance(String name){
        return this.balance;
    }

    public static void main(String[] args) {
        Demo demo = new Demo();
        new Thread(()->demo.set("aaa",100.0)).start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(demo.getBalance("aaa"));//

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(demo.getBalance("aaa"));
    }

}

6、T2线程能否执行?

public class Demo {

    int count = 0;

    synchronized void test(){
        System.out.println(Thread.currentThread().getName() + " start......");
        while (true) {
            count ++;
            System.out.println(Thread.currentThread().getName() + " count = " + count);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (count == 5) {
                //碰到异常的情况,如果没有处理,会自动释放锁,所以T2可以执行。
                int i = 1/0;
            }
        }
    }

    public static void main(String[] args) {
        Demo demo11 = new Demo();

        Runnable r = () -> demo11.test();

        new Thread(r, "t1").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(r, "t2").start();
    }

}

可以执行

7、volatile 关键字,使一个变量在多个线程间可见

public class Demo {

    boolean running = true;

    public void test(){
        System.out.println("test start...");
        while (running){

        }
        System.out.println("test end...");
    }

    public static void main(String[] args) {
        Demo demo = new Demo();

        new Thread(demo :: test,"t1").start();

        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        demo.running = false;
    }

}

mian,t1线程都用到一个变量,java默认是T1线程中保留一份副本,这样如果main线程修改了该变量,

t1线程未必知道

使用volatile关键字,会让所有线程都会读到变量的修改值

在下面的代码中,running是存在于堆内存的t对象中

当线程t1开始运行的时候,会把running值从内存中读到t1线程的工作区,在运行过程中直接使用这个副本,

并不会每次都去读取堆内存,这样,当主线程修改running的值之后,t1线程感知不到,所以不会停止运行

8、volatile不能替代synchronized或者说volatile保证不了原子性

public class Demo {

    volatile int count = 0;

    public void test(){
        for (int i = 0; i < 10000; i++) {
            count ++;
        }
    }


    public static void main(String[] args) {
        Demo demo = new Demo();

        List<Thread> threads = new ArrayList();

        for (int i = 0; i < 10; i++) {
            threads.add(new Thread(demo::test, "thread-" + i));
        }

        threads.forEach((o)->o.start());

        threads.forEach((o)->{
            try {
                o.join();
            } catch (Exception e) {
                e.printStackTrace();
            }
        });

        System.out.println(demo.count);
    }

}

比如说第一个线程加到100了,还没往上加,另外一个线程来了,把100拿过来执行方法,

然后第一个线程继续加到101,第二个线程也加到101,他两往回写都是101,线程不会管你加到哪儿了,

虽然说加了2但是实际上只加了1.

volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,

public class Demo {

    int count = 0;

    //相比较上一个例子,synchronized既保证了原子性又保证了可见性
    public synchronized void test(){
        for (int i = 0; i < 10000; i++) {
            count ++;
        }
    }

    public static void main(String[] args) {
        Demo demo = new Demo();

        List<Thread> threads = new ArrayList<Thread>();

        for (int i = 0; i < 10; i++) {
            threads.add(new Thread(demo::test, "thread-" + i));
        }

        threads.forEach((o)->o.start());

        threads.forEach((o)->{
            try {
                o.join();
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
        System.out.println(demo.count);
    }

}

能够保证输出结果10000

9、一道面试题:多个atomic类连续调用能否构成原子性?

public class Demo {

    AtomicInteger count = new AtomicInteger(0);
    public void test(){
        for (int i = 0; i < 10000; i++) {
            if(count.get() < 1000){
                count.incrementAndGet();
            }
        }
    }
    public static void main(String[] args) {
        Demo demo = new Demo();

        List<Thread> threads = new ArrayList();

        for (int i = 0; i < 10; i++) {
            threads.add(new Thread(demo::test, "thread-" + i));
        }

        threads.forEach((o)->o.start());

        threads.forEach((o)->{
            try {
                o.join();
            } catch (Exception e) {
                e.printStackTrace();
            }
        });

        System.out.println(demo.count);
    }

}

比如count加到999了,这时候一个线程拿到count判断,虽然.get方法保证原子性,但是他阻止不了其它线程也来判断,所以第一个线程还没加完,第二个线程也进来了,这时候两个线程都给count加了

10、一道面试题:实现一个容器,提供两个方法,add,size ,写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个时,线程2给出提示并结束线程2

public class ContainersCDL {
    volatile List lists = new ArrayList();
    public void add(Object o){
        lists.add(0);
    }

    public int size(){
        return lists.size();
    }

    public static void main(String[] args){
        ContainersCDL c = new ContainersCDL();
        CountDownLatch latch = new CountDownLatch(1);

        new Thread(()->{
            System.out.println("t2 start.....");
            if(c.size()!=5){
                try {
                    latch.await();
                }catch (Exception e){
                    e.printStackTrace();
                }
                System.out.println("t2 end....");
            }
        }," t2").start();

        new Thread(()->{
            System.out.println("t1 start...");
            for (int i =0;i<10;i++){
                c.add(new Object());
                System.out.println("add "+i);
                if(c.size()==5){
                    latch.countDown();
                }try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"t1").start();
    }
}

CountDownLatch 使用await和countdown方法替代wait(释放锁)和notify(不释放锁)

CountDownLatch不涉及锁定,当count的值为零时当前线程继续运行。相当于是发令枪,运动员线程调用await等待,计数到0开始运行

当不涉及同步,只是涉及线程通信的时候,用synchronized加wait,notify就显得太重了

###11、面试题:写一个固定容量同步容器,拥有Put和get方法,以及getCount方法,能够支持两个生产者线程以及10个消费者线程的阻塞调用

  • wait 阻塞

  • notifyAll会唤醒所有的线程,无法精准唤醒某一个线程

  • signalAll会唤醒指定的那些线程

public class ConditionLock<T> {
    private final LinkedList<T> lists = new LinkedList<>();
    private final int MAX = 10;
    private int count = 0;

    private Lock lock = new ReentrantLock();
    private Condition producer = lock.newCondition();
    private Condition consumer = lock.newCondition();

    public void put(T t){
        try {
            lock.lock();
            while (lists.size()==MAX){
                producer.await();
            }
            lists.add(t);
            ++count;
            consumer.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public T get(){
        T t = null;
        try {
            lock.lock();
            while (lists.size()==0){
                consumer.await();
            }
            t = lists.removeFirst();
            count--;
            producer.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
        return t;
    }
    public static void main(String[] args) {
        ConditionLock<String> c = new ConditionLock<>();
        for (int i = 0;i<100;i++){
            new Thread(()->{
                for (int j = 0;j<5;j++){
                    System.out.println("get: "+c.get());
                }
            },"c"+i).start();
        }
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        for (int i =0;i<2;i++){
            new Thread(()->{
                for (int j =0;j<25;j++){
                    c.put(Thread.currentThread().getName()+" "+j);
                }
            },"p"+i).start();
        }
    }
}

二、同步容器

1、ThreadLocal

只能在设置的线程中存在。线程的本地变量,与使用的线程绑定,其他线程不会get到。

线程并发高的情况下不要使用ThreadLocal

2、有10000张火车票,同时又10个窗口对外售票

# synchronized
public class SaleOfTickets {
    private static List<Integer> tickets = new LinkedList<>();

    static {
        for (int i=0;i<10000;i++){
            tickets.add(i);
        }
    }
    public static void main(String[] args) {
        for (int i = 0;i<10;i++){
            int finalI = i;
            new Thread(()->{
                while (true){
                    synchronized (tickets){
                        if (tickets.size()<=0){
                            break;
                        }
                        System.out.println(finalI +"窗口销售票编号:"+tickets.remove(0));
                    }
                }
            }).start();
        }
    }
}

这里使用synchronized使两个操作具备了原子性,不会出问题

public class ConcurrentLinkQueueTicket {
    private static Queue<Integer> tickets = new ConcurrentLinkedDeque<>();
    static {
        for (int i=0;i<10000;i++){
            tickets.add(i);
        }
    }
    public static void main(String[] args) {
        for (int i = 0;i<10;i++){
            int finalI = i;
            new Thread(()->{
                while (true){
                    Integer poll = tickets.poll();
                    if (poll==null){
                        break;
                    }
                    System.out.println(finalI +"窗口销售票编号"+poll);
                }
            }).start();
        }
    }
}

在JDK1.5以后,java里面提供了很多的并发容器,这里我们用的是一个queue,队列。

所谓队列其实就是一个容器,就是站成一对,不管票还是人都在里面排成一堆,队列有几种,有先进先出的,

还有两端的队列,还有就是栈,先进后出,先加进去的后出来。

这里用了一个concurrentlinkedqueue,并发的链表队列。线程里面调用了一个poll方法,

意思是往外面拿一个数据,相当于在尾巴里面拿一个,如果没有拿到,他的返回值就是空,那么就中断线程。

这里面没有加锁,同样有判断,但是不会出问题。完成卖票功能这种效率是比较高的。queue里面是不能装空值。

这里虽然判断和操作是一起的,但是我们没有在判断里面有任何操作,大不了反过头来再拿一边,

poll底层实现是cas,这里我们就不用加锁了。

3、CopyOnWriteList 写时复制容器

public class Demo {

	public static void main(String[] args) {
//		List lists = new ArrayList<>();
//		List lists = new Vector<>();
		List<String> lists = new CopyOnWriteArrayList<>();
		Random r = new Random();
		Thread[] threads = new Thread[100];
		
		for (int i = 0; i < threads.length; i++) {
			Runnable task = new Runnable() {
				@Override
				public void run() {
					for (int j = 0; j < 1000; j++) {
						lists.add("A" + r.nextInt(10000));
					}
				}
			};
			threads[i] = new Thread(task);
		}
		
		run(threads);
		
		System.out.println(lists.size());
	}

	private static void run(Thread[] threads) {
		long start = System.currentTimeMillis();
		Arrays.asList(threads).forEach(t->t.start());
		Arrays.asList(threads).forEach(t->{
			try {
				t.join();
			} catch (Exception e) {
				e.printStackTrace();
			}
		});
		long end = System.currentTimeMillis();
		System.out.println(end - start);
	}
}

在往集合中添加数据的时候,先拷贝存储的数组,然后添加元素到拷贝好的数组中,

然后用现在的数组去替换成员变量的数组(就是get等读取操作读取的数组)。

这个机制和读写锁是一样的,但是比读写锁有改进的地方,那就是读取的时候可以写入的 ,

这样省去了读写之间的竞争,看了这个过程,你也发现了问题,同时写入的时候怎么办呢,当然果断还是加锁。读多写少可以用copyonwritelist

4、Collections

public class Demo {

	public static void main(String[] args) {
		ArrayList<String> arrayList = new ArrayList<>();
		List<String> synchronizedList = Collections.synchronizedList(arrayList);
	}
}

collections是java里面一个集合处理类,里面有给容器加锁的方法,通过调用api可以返回一个加了锁的容器

5、Queue

public class Demo {

	public static void main(String[] args) {
		Queue<String> strings = new ConcurrentLinkedQueue<>();
		
		for (int i = 0; i < 10; i++) {
			//offer,类似于add方法,add会出一些问题,比如容量限制,
			//超出限制会抛异常,offer有返回值可以判断是否加成功了
			strings.offer("a" + i);
		}
		
		System.out.println(strings);
		
		System.out.println(strings.size());

		System.out.println(strings.poll());//拿了就没了
		System.out.println(strings.size());

		System.out.println(strings.peek());//用一下不删
		System.out.println(strings.size());
	}
}

6、ConcurrentHashMap

public class Demo {

	public static void main(String[] args) {
//		Map map = new ConcurrentHashMap<>();
		Map<String, String> map = new ConcurrentSkipListMap<>();
//		Map map = new Hashtable<>();

//		Map map = new HashMap<>();
//		Map map1 = Collections.synchronizedMap(map);

		Random random = new Random();
		Thread[] threads = new Thread[100];
		CountDownLatch latch = new CountDownLatch(threads.length);
		long start_time = System.currentTimeMillis();
		for (int i = 0; i < threads.length; i++) {
			threads[i] = new Thread(()->{
				for(int j=0; j<10000;j++) {
					map.put("a" + random.nextInt(100000), "a" + random.nextInt(100000));
//					map1.put("a" + random.nextInt(100000), "a" + random.nextInt(100000));
				}
				latch.countDown();
			});
		}
		Arrays.asList(threads).forEach(t->t.start());
		try {
			latch.await();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		long end_time = System.currentTimeMillis();
		System.out.println(end_time-start_time);
	}
}

第一种用hashtable,hashtable所有方法都加了锁了,

第二种concurrenthashmap,大致能看出来他的效率要比hashtable要高一些,在多线程的情况下。

为什么呢,因为hashtable往里面加任何数据的时候都是要锁定整个对象,

而concurrenthashmap,是分成十六个段,每次插数据的时候,只会锁住一小段,1.8之后实现不同。

7、LinkedBlockingQueue(阻塞式容器)

public class Demo {

	private static BlockingQueue<String> strings = new LinkedBlockingQueue<>(10);

	public static void main(String[] args) {
		new Thread(()->{
			for (int i = 0; i < 100; i++) {
				try {
					// 在阻塞式容器里面加了一个方法,put,也就是如果满了就会等待,对应的方法叫take,如果空了就会等待。
					// 这种容器我们去用的时候自动就实现了阻塞式的生产者消费者。
					strings.put("商品" + i);
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}, "producer").start();

		for (int i = 0; i < 5; i++) {
			new Thread(()->{
				for(;;){
					try {
						// take,拿,如果空了也会阻塞
						System.out.println(Thread.currentThread().getName() + " take " + strings.take()); //如果空了,就会等待
					} catch (Exception e) {
						e.printStackTrace();
					} 
				}
			},"consumer" + i).start();
		}

	}

}

8、ArrayBlockingQueue

public class Demo {

	private static BlockingQueue<String> strings = new ArrayBlockingQueue<>(10);
	
	public static void main(String[] args) throws InterruptedException {
		for (int i = 0; i < 10; i++) {
			strings.put("a" + i);
		}
		strings.add("aaaa");
//		strings.put("aaaa");
//		strings.offer("aaaa");
		strings.offer("aaaa",1, TimeUnit.SECONDS);
		System.out.println(strings);
	}
}

有界队列,意思就是说这个队列能装的元素的个数是固定的,后面讲线程池的时候,里面装的其实是一个个任务。

这里只能装10个,如果超过了可能会出问题可能会阻塞,这里看你调用什么方法。

add会报异常

offer不会报异常,他只通过布尔类型的返回值来告诉你是加成功了还是没有加成功。

offer可以设置时间,如果这段时间加不进去就不加了也就是返回false

put方法是满了会阻塞住。

9、DelayQueue

public class Demo {

	private static BlockingQueue<MyTask> tasks = new DelayQueue<>();

	static class MyTask implements Delayed{

		long runningTime;
		
		public MyTask(long rt) {
			this.runningTime = rt;
		}

		@Override
		public int compareTo(Delayed o) {
			if (this.getDelay(TimeUnit.MILLISECONDS) < o.getDelay(TimeUnit.MICROSECONDS)) {
				return -1;
			}else if (this.getDelay(TimeUnit.MILLISECONDS) > o.getDelay(TimeUnit.MILLISECONDS)) {
				return 1;
			}else {
				return 0;
			}
		}

		@Override
		public long getDelay(TimeUnit unit) {
			return unit.convert(runningTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
		}
		
		@Override
		public String toString() {
			return "" + runningTime;
		}
		
		public static void main(String[] args) throws InterruptedException {
			long now = System.currentTimeMillis();
			MyTask t1 = new MyTask(now+1000);
			MyTask t2 = new MyTask(now+2000);
			MyTask t3 = new MyTask(now+1500);
			MyTask t4 = new MyTask(now+2500);
			MyTask t5 = new MyTask(now+500);
			
			tasks.put(t1);
			tasks.put(t2);
			tasks.put(t3);
			tasks.put(t4);
			tasks.put(t5);
			
			System.out.println(tasks);
			
			for (int i = 0; i < 5; i++) {
				System.out.println(tasks.take());
			}
		}

	}

}

容器里每一个元素都设置了一个时间,时间到了才能从中提取元素

10、TransferQueue

public class Demo {

	public static void main(String[] args) throws InterruptedException {
		LinkedTransferQueue<String> strings = new LinkedTransferQueue<>();
		
		new Thread(()->{
			try {
				System.out.println("t1"+strings.take());
			} catch (Exception e) {
				e.printStackTrace();
			}
		}).start();

		new Thread(()->{
			try {
				System.out.println("t2"+strings.take());
			} catch (Exception e) {
				e.printStackTrace();
			}
		}).start();

		TimeUnit.SECONDS.sleep(2);

		strings.transfer("aaa");
//		strings.put("aaa");
		System.out.println(strings.size());
//		new Thread(()->{
//			try {
//				System.out.println(strings.take());
//			} catch (Exception e) {
//				e.printStackTrace();
//			}
//		}).start();
	}

}

和普通的queue的方法差不多,多了一个transfer方法。

如果你用这种队列的话,往往是消费者先启动,生产者生产一个东西的时候,他先是去找消费者,

如果有消费者就直接丢给消费者。

11、SynchronizedQueue

public class Demo {

	public static void main(String[] args) throws InterruptedException {
		BlockingQueue<String> strings = new SynchronousQueue<>();
		
		new Thread(()->{
			try {
				System.out.println(strings.take());
			} catch (Exception e) {
				e.printStackTrace();
			}
		}).start();
//		strings.add("aaa");
		strings.put("aaa");
		strings.put("aaa");
		strings.put("aaa");
		strings.put("aaa");
		strings.put("aaa");
		System.out.println(strings.size());
	}
}

同步队列是容量为0,也就是来的东西必须给消费掉.

首先启动一个消费者,调用add方法,他报错了

只能调用put,意思就是阻塞等待消费者消费。put里面其实用的是transfer,任何东西必须消费,不能往容器里面扔。

你可能感兴趣的:(Java)