多线程和常见同步场景

多线程

1、线程选择
多线程创建不要使用Executors去创建,而是通过ThreadPoolExecutor或AsyncTask(Android)构建线程池;使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。另外创建匿名线程不便于后续的资源使用分析,对性能分析等会造成困扰。
2、Executors对象弊端

ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(1);

2.1、FixedThreadPool 和SingleThreadPool : 允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM;

备注:因为线程数固定,但是可以无限添加任务;使用的BlockingQueue都是LinkedBlockingQueue

2.2、CachedThreadPool 和ScheduledThreadPool : 允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM
3、ThreadPoolExecutor构造函数解析

package java.util.concurrent;
 
public class ThreadPoolExecutor extends AbstractExecutorService {
       /**
         *  corePoolSize -  线程池中的核心线程数,即使线程空闲,也存在
         *  maximumPoolSize - 线程池中允许的最大线程数
         *  keepAliveTime -  当线程数大于核心线程数时,多余空闲线程在终止前等待新任务的最长时间
         *  unit -  keepAliveTime参数的时间单位
         *  workQueue - 在任务执行之前保存任务的队列;此队列将仅保存execute方法提交的Runnable任务
         *  threadFactory -  创建新线程的工厂
         *  handler - 达到线程边界和队列容量而阻止执行的处理程序
         */
      public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
            *******
      }
}

3.1、workQueue系统自带实现类LinkedBlockingDeque、SynchronousQueue、ArrayBlockingQueue
3.2、threadFactory 创建线程工厂,可以设置线程名称、优先级、是否守护线程
3.3、handler 系统自带的有AbortPolicy(抛出异常)、DiscardOldestPolicy(丢弃最旧的未处理请求,然后重试执行,除非执行程序关闭,在这种情况下,任务被丢弃)、DiscardPolicy(自动丢弃任务)
4、示例
4.1、正例

int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors();
int KEEP_ALIVE_TIME = 1;
TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS;
BlockingQueue<Runnable> taskQueue = new ArrayBlockingQueue<Runnable>(10);
ExecutorService executorService = new ThreadPoolExecutor(NUMBER_OF_CORES,
               NUMBER_OF_CORES*2, KEEP_ALIVE_TIME, KEEP_ALIVE_TIME_UNIT,
               taskQueue,Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());

4.2、反例

ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

new Thread(new Runnable() {
     @Override
     public void run() {
         //操作语句
           ...
     }
  }).start();

多线程同步

线程、驻内存、工作内存三者之间的关系
在这里插入图片描述
备注: 线程的工作内存保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存的变量。
线程间变量的值的传递均需要通过主内存完成。
线程安全的本质;在多个线程访问共同的资源时,在某⼀一个线程对资源进⾏行行写操作的中途(写⼊入已经开始,但还没结 束),其他线程对这个写了了⼀一半的资源进⾏行行了了读操作,或者基于这个写了了⼀一半的资源进⾏行行了了写操 作,导致出现数据错误
锁机制的本质:通过对共享资源进⾏行行访问限制,让同⼀一时间只有⼀一个线程可以访问资源,保证了了数据的准确性。
其核心就在于共享的资源。只要保证了原子性、可见行和有序性就是安全的
线程安全的实现方法: 互斥同步(悲观锁)、非阻塞同步(乐观锁)和无同步方案

2.0、操作共享的数据分类:
不可变: 基本数据类型定义为final就是是安全的;如果共享数据是一个对象,就需要保证对象的行为不会对其状态产生影响就可以,例如:可以把对象中带有状态的变量声明为final;枚举类等
绝对线程安全:java.util.Vector - 所有方法都是synchronized修饰,但是多线程不恰当时机remove时报错。
相对线程安全:Vector、HashTable等
线程兼容:对象本身并不是线程安全的,但是调用的正确使用同步手段保证对象在兵法环境中可以安全使用
线程对立:调用端无法在多线程环境中并发使用的代码; Thread类的suspend()和resume()方法
2.1、synchronized
保证⽅方法内部或代码块内部资源(数据)的互斥访问。即同⼀一时间、由同⼀一个Monitor 监视的代码,最多只能有⼀一个线程在访问

保证线程之间对监视资源的数据同步。即,任何线程在获取到 Monitor 后的第⼀一时间,会先将共享内存中的数据复制到⾃自⼰己的缓存中;任何线程在释放 Monitor 的第⼀一时间,会先将缓存中的数据复制到共享内存中。

public class TestSynchronized {
 
    private int x = 0;
    private int y = 0;
 
    private static final int NUM_MULTIPLE = 2;
 
    private String mName;
 
    private final Object object = new Object();
 
    private void setName(String name) {
        synchronized (object) {
            mName = name;
        }
    }
 
    private synchronized void count(int newValue) {
        x = newValue;
        y = newValue;
    }
 
    private void minus(int delta) {
        synchronized (this) {
            x = x - delta;
            y = y - delta;
        }
    }
   //
    private static synchronized int setTotal(int total) {
        total = total * 2;
        return total;
    }
 
    private static int setNum(int num){
        synchronized (TestSynchronized.class){
            num = num * 2;
            return num;
        }
 
    }
}

setName方法字节码示例如下:

0 aload_0
 1 getfield #5 <com/xiaoma/thread/TestSynchronized.object : Ljava/lang/Object;>
 4 dup
 5 astore_2
 6 monitorenter
 7 aload_0
 8 aload_1
 9 putfield #6 <com/xiaoma/thread/TestSynchronized.mName : Ljava/lang/String;>
12 aload_2
13 monitorexit
14 goto 22 (+8)
17 astore_3
18 aload_2
19 monitorexit
20 aload_3
21 athrow
22 return

2.2、volatile
保证加了了 volatile 关键字的字段的操作具有同步性,以及对 long 和 double 的 操作的原⼦子性(long double 原⼦子性这个简单说⼀一下就⾏行行)。因此 volatile 可以看做 是简化版的 synchronized

volatile 只对基本类型 (byte、char、short、int、long、float、double、 boolean) 的赋值操作和对象的引⽤用赋值操作有效,你要修改 User.name 是不不能保证同 步的

volatile 依然解决不不了了 ++ 的原⼦子性问题

应用场景:单例中的对象、布尔值
2.3、java.util.concurrent.atomic 包
有 AtomicInteger AtomicBoolean 等类,作⽤用和 volatile 基本⼀致;解决了++的原子性问题

public class TestAtomic {
 
    public static void main(String[] args) {
        ThreadFactory factory = new ThreadFactory() {
            AtomicInteger count = new AtomicInteger(0);
            @Override
            public Thread newThread(Runnable runnable) {
                count.getAndIncrement();
                return new Thread(runnable,"Thread-"+count);
            }
        };
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+" started!");
            }
        };
        Thread thread = factory.newThread(runnable);
        thread.start();
        Thread thread1 = factory.newThread(runnable);
        thread1.start();
    }
}

2.4、读写锁Lock / ReentrantReadWriteLock
主要用于读写锁,写入次数较少,读的次数较多;另外ReentrantLock相比synchronized增加了三个高级功能:等待可中断、可实现公平锁、锁可以绑定多个条件

public class TestLock {
 
    ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true); //公平锁
    Lock readLock = lock.readLock();
    Lock writeLock = lock.writeLock();
 
    private int x = 0;
 
    private void count() {
        writeLock.lock();
        try {
            x++;
        } finally {
            writeLock.unlock();
        }
    }
 
    private void print(int time) {
        try {
           readLock.tryLock(500, TimeUnit.SECONDS); //等待可中断锁
            for (int i = 0; i < time; i++) {
                System.out.println(i + "");
            }
            System.out.println("");
        }  catch (InterruptedException e) {
            System.out.println("InterruptedException");
        } finally {
            readLock.unlock();
        }
    }
}

finally 的作⽤用:保证在⽅方法提前结束或出现 Exception 的时候,依然能正常释 放锁
2.5、CountDownLatch
使用场景是它允许一个或多个线程一直等待,直到其他线程的操作执行完后再执行
示例:


public class TestJUC {
 
    public static void main(String[] args) {
        final CountDownLatch latch = new CountDownLatch(2);
        ExecutorService service = Executors.newFixedThreadPool(3);
        service.execute(new Runnable() {
            @Override
            public void run() {
                try {
                   Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程1"+Thread.currentThread().getName());
                latch.countDown();
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程2"+Thread.currentThread().getName());
                latch.countDown();
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println("await");
                    latch.await();
                    System.out.println("await - 结束");
                } catch (InterruptedException e) {
                    System.out.println("await - InterruptedException");
                }
                System.out.println("await - 完成");
            }
        });
    }
}

运行结果:

线程2pool-1-thread-2
await
线程1pool-1-thread-1
await - 结束
await - 完成

2.6、CyclicBarrier
应用场景:玩dota、LoL和王者荣耀游戏为例,第一个玩家准备了之后,还需要等待其他4个玩家都准备,游戏才可继续,否则准备的玩家会被一直处于等待状态,只有当最后一个玩家准备了之后,游戏才会继续执行

示例代码:

public class TestCyclicBarrier {
 
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(5);
        final CyclicBarrier barrier = new CyclicBarrier(3);
        threadPool.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                try {
                    System.out.println(Thread.currentThread().getName() +"线程开始等待");
                    barrier.await();
                    System.out.println(Thread.currentThread().getName() +"线程结束");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }
        });
        threadPool.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println(Thread.currentThread().getName() + "线程开始等待");
                    barrier.await();
                    System.out.println(Thread.currentThread().getName() + "线程结束");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }
        });
        try {
            System.out.println(Thread.currentThread().getName() + "线程开始等待");
            barrier.await();
            System.out.println(Thread.currentThread().getName() + "线程完成");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (BrokenBarrierException e) {
            e.printStackTrace();
        }
    }
}

运行结果:

pool-1-thread-2线程开始等待
main线程开始等待
pool-1-thread-1线程开始等待
 
pool-1-thread-1线程结束
pool-1-thread-2线程结束
main线程完成

2.7、Semaphore
应用场景:比如银行柜台的窗口,一共有5个窗口可以使用,当窗口都被占用时,后面来的人就需要排队等候,直到有窗口用户办理完业务离开之后后面的人才可继续争取


public class TestSemaphore {
    public static void main(String[] args) {
        final Semaphore semaphore = new Semaphore(3);
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 7; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println("用户"+Thread.currentThread().getName()+"占用窗口");
                        semaphore.acquire(1);
                        System.out.println("用户"+Thread.currentThread().getName()+"正在办理业务");
                        Thread.sleep(2000L);
                        semaphore.release();
                        System.out.println("用户"+Thread.currentThread().getName()+"离开窗口");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
}

示例结果:

用户pool-1-thread-1占用窗口
用户pool-1-thread-3占用窗口
用户pool-1-thread-5占用窗口
用户pool-1-thread-2占用窗口
用户pool-1-thread-4占用窗口
用户pool-1-thread-6占用窗口
用户pool-1-thread-7占用窗口
用户pool-1-thread-2正在办理业务
用户pool-1-thread-1正在办理业务
用户pool-1-thread-4正在办理业务
用户pool-1-thread-4离开窗口
用户pool-1-thread-6正在办理业务
用户pool-1-thread-2离开窗口
用户pool-1-thread-3正在办理业务
用户pool-1-thread-5正在办理业务
用户pool-1-thread-1离开窗口
用户pool-1-thread-6离开窗口
用户pool-1-thread-5离开窗口
用户pool-1-thread-3离开窗口
用户pool-1-thread-7正在办理业务
用户pool-1-thread-7离开窗口

2.8、ThreadLocal
应用场景:每个线程保存不同的树枝,互相之间没有影响,比如:Handler
示例:

final ThreadLocal<String> local = new ThreadLocal<>();
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        local.set(Thread.currentThread().getName()+"你好");
        threadPool.execute(new Runnable() {
            @Override
            public void run() {
                local.set(Thread.currentThread().getName()+"线程1");
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"/11/"+local.get());
            }
        });
        threadPool.execute(new Runnable() {
            @Override
            public void run() {
                local.set(Thread.currentThread().getName()+"线程2");
                System.out.println(Thread.currentThread().getName()+"/22/"+local.get());
            }
        });
        System.out.println(Thread.currentThread().getName()+"/"+local.get());

示例结果:

main/main你好
pool-1-thread-2/22/pool-1-thread-2线程2
pool-1-thread-1/11/pool-1-thread-1线程1

备注:参考【深入理解Java虚拟机】

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值