最近几个月终于有大把时间总结这两年来所学 2019.5.29
前言
java中的多线程包括下面两点
- 多线程怎么用
- 线程安全
区分几个概念
区别一下 进程、线程、CPU线程、操作系统线程
- 进程:操作系统中一块独立的区域,和操作系统独立开,数据不共享,相互隔离。
- 线程:工作在进程中的工作单元,可以共享资源。
- CPU线程:CPU在硬件级别同时能做的事情(注意是硬件层面,而非软件上做的时间切片)。有做过单片机的裸机代码的同学,对这个概念应该会非常熟悉。
- 操作系统线程:模仿的CPU线程,把CPU的线程做进一步拆分,分为一个个的时间切片,轮询的做各种不同的工作
- 区别线程和进程,本质上来说,线程(thread)和进程(Process)不是一个概念,进程本身是一个运行的程序,而线程是程序中的工作单元。Thread 的英文本意是棉毛线的意思,而Process是过程,工序的意思。对比一下两者的英文意思,可以很好地理解两个概念。
- 线程池的大小和CPU的核心数。一般线程池的大小和CPU的核心数要成正相关
java中实现多线程
简要列举一下java中常用的多线程的实现。
- Thread 创建一个thread,重写run方法。然后thread.start();
static void thread() {
Thread thread = new Thread() {
@Override
public void run() {
System.out.println("use thread");
}
};
thread.start();
}
复制代码- runnable
static void runnable() {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("use runnable");
}
};
Thread thread = new Thread(runnable);
thread.start();
}
复制代码Runnable 和 Thread两个方法关联是很紧密的,我最近看了看源码,发现,Thread里面的run方法是长这个样子的
public void run() {
if (this.target != null) {
this.target.run();
}
}
复制代码然后我们看到里面有一个target,这个target就是 Thread thread = new Thread(runnable);里面的runnable。 所以对于Thread和Runnable两个方法来说,我们如果重写了Runnable,就会运行runnable的run方法,如果写了Thread的run方法,就会运行Thread的run方法。如果两个都写了,两个都会运行。 3. ThreadFactory
static void threadFactory() {
ThreadFactory factory = new ThreadFactory() {
AtomicInteger count = new AtomicInteger(0);
// int count = 0;
@Override
public Thread newThread(Runnable r) {
// count++;
return new Thread(r, "Thread-" + count.incrementAndGet());
}
};
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();
}
复制代码这种方法用的比较少,简单看看就可以。
- Executor 线程池 这是非常非常非常常用的方法。有四种常用的方法
- 基本用法
static void executor() {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Thread with Runnable started!");
}
};
Executor executor = Executors.newCachedThreadPool();
executor.execute(runnable);
}
复制代码- 基本构造方法
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue)
变量含义分别为:
核心线程数;
最大线程数;
keep alive 的时间;单位;
装runnable的队列
复制代码几个变量的判断优先级为:corePoolSize > workQueue > maximumPoolSize; 解释一下: 当前线程数 < corePoolSize,新任务来临,无论是否有空的线程任务,都会优先创建核心线程; corePoolSize < 当前线程数 < maximumPoolSize,只有当workQueue满了,才会创建新的线程去做任务,否则都将默认排队执行;
- 下面就这个构造基本的构造方法来介绍一下我们常用的四中线程池。
a.newCachedThreadPool:
new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue());
//核心线程数:0;最大线程数:无穷大;等待时间:60S
复制代码从名字也可以看出,这是可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,如果没有空线程等待回收,则新建线程。
b.newSingleThreadPool :
new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());
//核心线程数:1;最大线程数1;永不回收
复制代码特点:线程一直存在,随时可以拿来用。可以用来做取消功能。
c.newFixedThreadPool
new ThreadPoolExecutor(32, 32, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());
//核心线程数:自定义;最大线程数:核心线程数;不回收
复制代码做集中的事情,比如下载文件,诉求就是,我需要马上用很多线程,而且就用这么一下,用完了我就不要了。
d.newSchedualThreadPool:
new ThreadPoolExecutor(32, 2147483647, 0L, TimeUnit.NANOSECONDS, new ScheduledThreadPoolExecutor.DelayedWorkQueue());
复制代码可以设置定时,以及周期性执行任务。
- callable:一个有返回值的runnable 后台任务在后台执行,返回值返回给谁?和Future类配合使用,将返回的值给future的get方法。如下面的程序段所示 注意,Future.get是一个阻塞方法,会一直等到callable执行完成,才会返回数据。
static void callable() {
Callable<String> callable = new Callable<String>() {
@Override
public String call() {
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Done";
}
};
ExecutorService executor = Executors.newCachedThreadPool();
Future<String> future = executor.submit(callable);
try {
String result = future.get();
System.out.println("result: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
复制代码synchronized
- 原子操作:CPU级别,只执行一次的代码。不可被拆分
- synchronized的互斥 为了保护方法或者代码块内的数据,重点是保护数据。我们关注的不是方法,而是资源,只有处理同样数据方法,才需要做互斥操作 每一个synchronized都有对应一个monitor, 只有相同的monitor才具有互斥性。 非静态方法的monitor是类的实例化对象。静态方法的monitor是类。
public static synchronized void methodA(){
//do something
}
public synchronized void methodB(){
//do something
}
private final Object monitorC = new Object();
public void methodC(){
synchronized (monitorC){
//do something
}
}
复制代码看上面的一段代码,methodA/methodB/methodC三个方法,都加了synchronized,但是他们的monitor分别是:className.class,this,monitorC,所以三个方法可以同时被调用。我们的锁,是针对每个monitor进行锁的,两个方法在同一个monitor下,可以达到锁的作用,如果不在一个monitor下,就达不到锁的作用。同时,对于不是操作同样数据的方法,是没有必要加互斥锁的
- 死锁
- 两个锁一起使用的时候,会出现死锁。比如下面这中情况
public class DeadLock {
private final Object monitor1 = new Object();
private final Object monitor2 = new Object();
private void setName(int name){
synchronized (monitor1){
name = 1;
//线程1运行到此处的时候,被线程2打断,跑到setAge中
synchronized (monitor2){
name = 2;
}
}
}
private void setAge(int age){
synchronized (monitor2){
age = 1;
//线程是运行到此处,就会出现死锁
synchronized (monitor1){
age = 2;
}
}
}
}
复制代码上面的代码中,注释里面有详细说明。monitor1和monitor2在相互等待对方运行完成,导致死锁产生。
- 乐观锁、悲观锁 关于数据库读写的问题。比如账户余额,用于存入100块,需要先读出原来的金额,然后+100。但是该操作可能会同时进行,比如两个人同时给你转账。 乐观锁:写入数据前,先核对一下数据有没有变化,如果有变化,那么就加一个锁。 悲观锁:无论有没有人写,都先把把锁加上。
volatile
- 相当于一个轻量级的synchronized。保证修饰的变量具有同步性,即被修饰的变量被赋值时,具有原子性。无法解决++的问题。
其他
- 解决++的非原子问题:使用Atomic
看看上面的threadFactory的示例代码中。
- 读写锁 上面有说道,同步方法本质是为了保护方法中处理的数据。所以我们在读写方法中,使用读写锁来解决synchronized过重的问题
public class ReadWriteLockDemo {
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
private ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
private int x = 0;
private void write() {
writeLock.lock();
try {
x++;
} finally {
writeLock.unlock();
}
}
private void read(int time) {
readLock.lock();
try {
for (int i = 0; i < time; i++) {
System.out.print(x + " ");
}
System.out.println();
} finally {
readLock.unlock();
}
}
}
复制代码- 以上为线程的相关简要整理。
















