一、创建线程和线程池时要指定有意义的名称
创建线程时要指定有意义的名称,以便问题追溯。
A、线程名
public static void main(String[] args)
{
//订单模块
Thread threadOne = new Thread(new Runnable()
{
public void run()
{
System.out.println("保存订单的线程");
try
{
Thread.sleep(500);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
throw new NullPointerException();
}
});
//发货模块
Thread threadTwo = new Thread(new Runnable()
{
public void run()
{
System.out.println("保存收获地址的线程");
}
});
threadOne.start();
threadTwo.start();
}
以上代码分别创建了线程one和线程two并且启动执行,运行上面代码可能会有如下输出:
保存订单的线程
保存收获地址的线程
Exception in thread "Thread-0" java.lang.NullPointerException
at top.cfish.java.ext.Loop$1.run(Loop.java:34)
at java.lang.Thread.run(Thread.java:748)
从运行结果可知Thread-0抛出了NullPointerException异常,但是从日志本无法判断是订单模块的线程抛出的异常。
当一个系统中有多个业务模块而每个模块中又都是用了自己的线程,除非抛出与业务相关的异常,否则根本没法判断是哪一个模块出现了问题。
static final String THREAD_SAVE_ORDER = "THREAD_SAVE_ORDER";
static final String THREAD_SAVE_ADDR = "THREAD_SAVE_ADDR";
public static void main(String[] args)
{
// 订单模块
Thread threadOne = new Thread(new Runnable()
{
public void run()
{
System.out.println("保存订单的线程");
throw new NullPointerException();
}
}, THREAD_SAVE_ORDER);
// 发货模块
Thread threadTwo = new Thread(new Runnable()
{
public void run()
{
System.out.println("保存收货地址的线程");
}
}, THREAD_SAVE_ADDR);
threadOne.start();
threadTwo.start();
}
从运行结果就可以定位到是保存订单模块抛出了NullPointerException异常。
保存订单的线程
保存收货地址的线程
Exception in thread "THREAD_SAVE_ORDER" java.lang.NullPointerException
at top.cfish.java.ext.Loop$1.run(Loop.java:29)
at java.lang.Thread.run(Thread.java:748)
B、线程池名
一般一个应用中会创建不止一个线程池,为了业务隔离,一般不同的业务使用不同的线程池,同样不指定线程池名会很难难定位问题。
static ThreadPoolExecutor executorOne = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>());
static ThreadPoolExecutor executorTwo = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>());
public static void main(String[] args)
{
//接受用户链接模块
executorOne.execute(new Runnable()
{
public void run()
{
System.out.println("接受用户链接线程");
throw new NullPointerException();
}
});
//具体处理用户请求模块
executorTwo.execute(new Runnable()
{
public void run()
{
System.out.println("具体处理业务请求线程");
}
});
executorOne.shutdown();
executorTwo.shutdown();
}
运行结果,并不知道是哪个模块的线程池抛出的异常。
接受用户链接线程
具体处理业务请求线程
Exception in thread "pool-1-thread-1" java.lang.NullPointerException
at top.cfish.java.ext.Loop$1.run(Loop.java:29)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
默认线程池使用DefaultThreadFactory来创建线程和指定名称,所以可以定制线程工厂。
// 命名线程工厂
static class NamedThreadFactory implements ThreadFactory
{
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
NamedThreadFactory(String name)
{
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
if (null == name || name.isEmpty())
{
name = "pool";
}
namePrefix = name + "-" + poolNumber.getAndIncrement() + "-thread-";
}
public Thread newThread(Runnable r)
{
Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0);
if (t.isDaemon())
{
t.setDaemon(false);
}
if (t.getPriority() != Thread.NORM_PRIORITY)
{
t.setPriority(Thread.NORM_PRIORITY);
}
return t;
}
}
static ThreadPoolExecutor executorOne = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>(), new NamedThreadFactory("ASYN-ACCEPT-POOL"));
static ThreadPoolExecutor executorTwo = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>(), new NamedThreadFactory("ASYN-PROCESS-POOL"));
public static void main(String[] args)
{
//接受用户链接模块
executorOne.execute(new Runnable()
{
public void run()
{
System.out.println("接受用户链接线程");
throw new NullPointerException();
}
});
//具体处理用户请求模块
executorTwo.execute(new Runnable()
{
public void run()
{
System.out.println("具体处理业务请求线程");
}
});
executorOne.shutdown();
executorTwo.shutdown();
}
从ASYN-ACCEPT-POOL-1-thread-1就可以知道是接受连接线程池抛出的异常。
接受用户链接线程
Exception in thread "ASYN-ACCEPT-POOL-1-thread-1" java.lang.NullPointerException
具体处理业务请求线程
at top.cfish.java.ext.Loop$1.run(Loop.java:66)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
二、不要new线程,要用线程池
不要new线程,要用线程池。
首先线程的创建和销毁是有系统开销的,线程是系统很宝贵的资源,如果无限制地创建会把系统资源消耗殆尽,其次我们来看看线程池的好处,线程池主要解决以下两个问题。
A、一方面,当执行大量异步任务时线程池能够复用线程,提供较好的性能,在不使用线程池的时候,每当需要执行异步任务时是直接new一线程进行运行,然后任务执行完毕后被销毁回收,而线程的创建和销毁是需要开销的。
static class Demo
{
private int index;
public void doTask()
{
System.out.println("i:" + index + " doTask() called");
}
public Demo(int index)
{
this.index = index;
}
}
static void process(Demo demo)
{
new Thread(new Runnable()
{
@Override
public void run()
{
try
{
demo.doTask();
}
catch (Exception e)
{
System.out.println(e.getLocalizedMessage());
}
}
}).start();
}
public static void main(String[] args) throws InterruptedException
{
for (int i = 0; i < 1000; ++i)
{
Demo demo = new Demo(i);
process(demo);
}
}
使用线程池的时候,线程池里面的线程是可复用的,不会每次执行异步任务时都重新创建和销毁线程。
static class Demo
{
private int index;
public void doTask()
{
System.out.println("i:" + index + " doTask() called");
}
public Demo(int index)
{
this.index = index;
}
}
static ThreadPoolExecutor threadPool = new ThreadPoolExecutor(8, 8, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<Runnable>());
static void process(Demo demo)
{
threadPool.execute(new Runnable()
{
@Override
public void run()
{
try
{
demo.doTask();
}
catch (Exception e)
{
System.out.println(e.getLocalizedMessage());
}
}
});
}
public static void main(String[] args) throws InterruptedException
{
for (int i = 0; i < 1000; ++i)
{
Demo demo = new Demo(i);
process(demo);
}
threadPool.shutdown();
}
B、另一方面,线程池提供了一种资源限制和管理的手段,比如可以限制线程的个数、动态新增线程等,每个ThreadPoolExecutor也保留了一些基本的统计数据。
其中getActiveCount返回当前激活的线程个数(正在执行任务的线程),getCompletedTaskCount返回已经完成的任务个数,getTaskCount返回所有任务个数,包含已经完成的、正在执行的和队列里面缓存的。
三、线程池不建议使用工具类Executors创建
不允许使用工具类Executors创建线程池,而是通过手动构造ThreadPoolExecutor来创建。
JDK提供Executors类的目的是为了便于开发人员创建线程池,通过Executors的下列方法可以创建不同用途的线程池。
A、newFixedThreadPool
创建一个核心线程个数和最大线程个数都为nThreads的线程池,并且阻塞队列长度为 Integer.MAX_VALUE,keepAliveTime=0 说明只要线程个数比核心线程个数多并且当前空闲则回收。
public static ExecutorService newFixedThreadPool(int nThreads)
{
return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}
//使用自定义线程创建工厂
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory)
{
return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), threadFactory);
}
B、newSingleThreadExecutor
创建一个核心线程个数和最大线程个数都为1的线程池,并且阻塞队列长度为 Integer.MAX_VALUE,keepAliveTime=0 说明只要线程个数比核心线程个数多并且当前空闲则回收。
public static ExecutorService newSingleThreadExecutor()
{
return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()));
}
//使用自己的线程工厂
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory)
{
return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), threadFactory));
}
C、newCachedThreadPool
创建一个按需创建线程的线程池,初始线程个数为0,最多线程个数为 Integer.MAX_VALUE,并且阻塞队列为同步队列,keepAliveTime=60 说明只要当前线程60s内空闲则回收。这个的特殊性在于加入到同步队列的任务会马上被执行,同步队列里面最多只有一个任务。
public static ExecutorService newCachedThreadPool()
{
return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
}
//使用自定义的线程工厂
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory)
{
return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), threadFactory);
}
可知Executors类只是ThreadPoolExecutor的一个门面,开发人员可以很方便地使用它来创建线程池。
比如创建一个单个线程的线程池,使用Executors工具类:
final static ExecutorService pool = Executors.newSingleThreadExecutor();
如果不用工具类需要用下面的方式创建:
final static ThreadPoolExecutor pool = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
上面两者是等价的,但是使用Executors创建的线程池,屏蔽了太多细节,比如阻塞队列大小,默认是无界限,那么可能有些开发人员就不会注意到这个,从而导致阻塞队列堆积了大量元素,导致OOM。
《阿里巴巴Java开发手册》规定开发人员创建线程池时使用后者来创建,因为创建时需要开发人员清楚地指定线程池中核心线程个数、最大线程个数、非活跃线程生存周期、阻塞队列类型和拒绝策略。开发人员指定这些参数的时候必须根据具体场景设置最合适的参数,从而规避资源耗尽的风险。
四、SimpleDateFormat是线程不安全的类
//(1)创建单例实例
static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args)
{
// (2)创建多个线程,并启动
for (int i = 0; i < 10; ++i)
{
Thread thread = new Thread(new Runnable()
{
public void run()
{
try
{
// (3)使用单例日期实例解析文本
System.out.println(sdf.parse("2017-12-13 15:17:27"));
}
catch (Exception e)
{
e.printStackTrace();
}
}
});
thread.start();// (4)启动线程
}
}
一个SimpleDateFormat实例,10个线程,每个线程都共用同一个sdf对象对文本日期进行解析,多运行几次就会抛出java.lang.NumberFormatException异常,加大线程的个数更容易复现。
Javadoc里面也有说明:
Date formats are not synchronized. It is recommended to create separate format instances for each thread. If multiple threads access a format concurrently, it must be synchronized externally.
SimpleDateFormat是线程不安全的,其中存在共享变量,并且没有在访问共享变量前进行适当的同步处理。
如果一定要使用SimpleDateFormat,有以下方案。
A、定义工具类
每次使用时返回一个SimpleDateFormat实例,保证每个实例使用自己的Calendar实例,但是每次使用都需要new一个对象,并且使用后由于没有其他引用,就会需要被回收,会增加开销。
static SimpleDateFormat getSimpleDateFormatInstance()
{
return new SimpleDateFormat();
}
B、同步
多线程中可以使用synchronized进行同步。
// (1)创建单例实例
static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args)
{
// (2)创建多个线程,并启动
for (int i = 0; i < 10; ++i)
{
Thread thread = new Thread(new Runnable()
{
public void run()
{
try
{
// (3)使用单例日期实例解析文本
synchronized (sdf)
{
System.out.println(sdf.parse("2017-12-13 15:17:27"));
}
}
catch (ParseException e)
{
e.printStackTrace();
}
}
});
thread.start();// (4)启动线程
}
}
使用同步意味着多个线程要竞争锁,在高并发场景下会导致系统响应性能下降。
对于多线程环境,更推荐使用JodaTime进行处理。
五、高并发时考虑锁的性能损耗
高并发时,同步调用应考虑锁的性能,能用无锁,就不用锁,一定要用锁时候,加锁粒度尽量小。
在多线程编程中,少不了多个线程需要并发访问一个共享资源的情况,为避免多线程访问共享资源时的并发问题,一般需要在访问共享资源前进行适当同步,比如在处理共享资源前加独占锁,再处理资源,然后释放锁,但是锁的粒度使用不当,会影响并发性能。
一个共享变量,只有修改获取操作,并且共享变量的当前值并不依赖原来的值,这时候可以使用synchronized进行同步。
public class SharedValue
{
private int val;
public synchronized int getVal()
{
return val;
}
public synchronized void setVal(int val)
{
this.val = val;
}
}
因为进入synchronized块前会清空锁块内本地内存中将会用到的共享变量,所以getVal方法获取的val是直接从主内存获取的,由于退出synchronized会把锁块内修改的本地变量刷新到主内存,从而保证了共享变量的内存可见性。
但是synchronized块加的是独占锁,这导致线程只能有一个线程获取锁调用getVal方法获取当前变量的值,而getVal本身并不会修改val的值,所以这大大降低了读取的并发性。
既然这里变量val的值并不依赖原来的值,那么这里其实使用无锁volatile修饰val变量就可以解决内存不可见性问题。
public class SharedValue
{
private volatile int val;
public int getVal()
{
return val;
}
public void setVal(int val)
{
this.val = val;
}
}
但是volatile只能保证内存可见性,不能保证原子性。
public class UnSafeCount
{
private volatile int val;
public int getVal()
{
return val;
}
public void inc()
{
++val;
}
}
由于++val不具有原子性,所以该计数器不是线程安全的,应该保证inc方法是原子性的,这时候synchronized就派上用场了(synchronized保证原子性和内存可见性)。
public class SafeCount
{
private int val;
public synchronized int getVal()
{
return val;
}
public synchronized void inc()
{
++val;
}
}
但是由于synchronized是独占锁,导致同时调用getVal方法读取val值的线程竞争独占锁,这降低了并发度,这时候可以降低锁的范围,让读写锁分离。
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
private final ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
private int val;
public int getVal()
{
readLock.lock();
try
{
return val;
}
finally
{
readLock.unlock();
}
}
public void inc()
{
writeLock.lock();
try
{
++val;
}
finally
{
writeLock.unlock();
}
}
使用读写分离锁,多个线程可以同时获取读锁然后获取val,增加了读取的并发度,但是上面的策略还是使用锁,其实关于计数器,JUC包提供了无锁CAS算法的实现,比如AtomicInteger、AtomicLong等类,如果在高并发下,大量线程在进行CAS失败后本地自旋重试,可以使用LongAdder。
六、并发修改同一条记录时需要加锁以避免更新丢失
并发修改同一条记录时需要加锁以避免更新丢失。锁可以是应用层的,可以是缓存层,也可以是数据库层。
多线程并发修改同一个记录的情况在项目实践中很常见,为了避免数据更新丢失,一般有两种方式,一种是悲观锁,一种是乐观锁。
如果每次访问冲突概率小于20%,建议使用乐观锁,这是因为乐观锁本身不需要对行记录加锁,并发性能较好,另外使用乐观锁时重试次数不少于3次。如果每次访问冲突概率比较大,那么使用乐观锁会导致重试次数比较高,并且等达到重试次数后,也不一定更新OK,所以这时候建议使用悲观锁。
对于应用层,一般是使用分布式锁来做同步,缓存上可以使用Redis的CAS操作来做同步。
七、使用ScheduledExecutorService替代Timer
Timer下启动多个任务,只要其中一个任务抛出了异常,其他任务会自动终止,ScheduledExecutorService就不会有这个问题。
// 创建定时器对象
static Timer timer = new Timer();
public static void main(String[] args)
{
// 添加任务1,延迟500ms执行
timer.schedule(new TimerTask()
{
@Override
public void run()
{
System.out.println("---one Task---");
try
{
Thread.sleep(1000);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
throw new RuntimeException("error ");
}
}, 500);
// 添加任务2,延迟1000ms执行
timer.schedule(new TimerTask()
{
@Override
public void run()
{
for (; ; )
{
System.out.println("---two Task---");
try
{
Thread.sleep(1000);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
}, 1000);
}
先添加了一个任务在500ms后执行,然后添加了第二个任务在1s后执行。
期望的是当第一个任务输出---one Task---,后等待1s;第二个任务会输出---two Task---。
实际执行结果,第一个任务抛异常后,第二个任务就终止了。
---one Task---
Exception in thread "Timer-0" java.lang.RuntimeException: error
at top.cfish.java.ext.TestTimer$1.run(TestTimer.java:33)
at java.util.TimerThread.mainLoop(Timer.java:555)
at java.util.TimerThread.run(Timer.java:505)
Timer内部当任务执行过程中抛出了除InterruptedException之外的异常后,唯一的消费线程就会因为抛出异常而终止,那么队列里面的其他待执行的任务就会被清除。
要实现类似Timer的功能使用ScheduledThreadPoolExecutor的schedule是比较好的选择。ScheduledThreadPoolExecutor中的一个任务抛出了异常,其他任务不受影响。
static ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1);
public static void main(String[] args)
{
scheduledThreadPoolExecutor.schedule(new Runnable()
{
@Override
public void run()
{
System.out.println("---one Task---");
try
{
Thread.sleep(1000);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
throw new RuntimeException("error ");
}
}, 500, TimeUnit.MICROSECONDS);
scheduledThreadPoolExecutor.schedule(new Runnable()
{
@Override
public void run()
{
for (int i = 0; i < 2; ++i)
{
System.out.println("---two Task---");
try
{
Thread.sleep(1000);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
}, 1000, TimeUnit.MICROSECONDS);
scheduledThreadPoolExecutor.shutdown();
}
执行结果符合预期。
---one Task---
---two Task---
---two Task---
Process finished with exit code 0