多线程与锁的认识
线程
线程:是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程(单线程程序)。一个 进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
多线程:多线程就是指一个进程中同时有多个线程在执行,多线程的好处是提高执行效率但是会容易造成死锁的情况。
多线程的优缺点
- 优点
- 使用线程可以把占据长时间的程序中的任务放到后台去处理
- 加快程序的运行速度
- 可以让同一个程序的不同部分并发执行
- 使用多线程可以将耗时操作放在后台继续执行的同时执行其他操作提高效率
- 缺点
- 因为多线程需要开辟内存,而且线程切换需要时间因此会很消耗系统内存。
- 线程的终止会对程序产生影响
- 由于多个线程之间存在共享数据,因此容易出现线程死锁的情况
线程的五大状态
线程的五大状态:创建状态、就绪状态、运行状态、阻塞状态、死亡状态
创建状态:Thread thread = new Thread(); 线程对象一旦创建就进入到新生状态(每一个对象都有一把锁)
就绪状态:当线程对象调用start()方法,线程立即进入就绪状态,但不意味着立即调度执行(线程的调度有cpu负责,自动选择合适的调度模式进行线程的调度)
阻塞状态:当调用sleep, wait 或同步锁定时,线程进入阻塞状态,就是代码不往下执行,阻塞事件解除后,重新进入就绪状态,等待cpu调度执行。
运行状态:进入运行状态,线程才真正执行线程体的代码块。
死亡状态:线程中断或者结束,一旦进入死亡状态,就不能再次启动。
线程创建的三种方式
- hread class 继承Thread类(重点)
- Runnable接口(重点)
- Callable实现Callable接口(了解)
继承Thread类
Thread类本身实现了Runnable接口
直接调用run方法和start方法的区别:直接调用run方法相当于是调用了一个普通的方法,而调用start方法才相当于是开启了一个线程。当我们在main线程与分线程分别执行2000次输出语句,那么就会发现两个输出语句混杂在一起,也就是多线程。
实现Runnable接口
Java的设计是单继承的设计,如果采用继承Thread的方式实现多线程,则不能继承其他的类。
实现Callable接口
线程run方法没有返回值,如果一个线程需要有返回值时,可以采用实现Callable接口来实现多线程。
-
实现Callable接口,需要返回值类型
-
重写call方法,需要抛出异常
-
创建目标对象
-
创建执行服务:ExecutorService ser = Executors.newFixedThreadPool(1);
-
提交执行:Future result1 = ser.submit(11);
-
获取结果:boolean r1 = result1.get()
-
关闭服务:ser.shutdownNow();
探索Callable接口的源码
@FunctionalInterface
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
守护线程
什么是守护线程
Java线程分为用户线程和守护线程。
守护线程是程序运行的时候在后台提供一种通用服务的线程。所有用户线程停止,进程会停掉所有守护线程,退出程序。
Java中把线程设置为守护线程的方法:在 start 线程之前调用线程的 setDaemon(true) 方法
虚拟机必须确保用户线程执行完毕,虚拟机不用等待守护线程执行完毕。如:后台记录操作日志,监控内存,垃圾回收等待。
注意:
- setDaemon(true) 必须在 start()之前设置,否则会抛出IllegalThreadStateException异常,该线程仍默认为用户线程,继续执行
- 守护线程创建的线程也是守护线程
- 守护线程不应该访问、写入持久化资源,如文件、数据库,因为它会在任何时间被停止,导致资源未释放、数据写入中断等问题
线程同步机制
方式一:synchronized关键字
synchronized可以阻止并发更新同一个共享资源,实现同步
synchronized不能用来实现不同线程之间的消息传递(通信)
由于我们可以通过private关键字来保证数据对象只能被方法访问,所以我们只需要针对方法提出一套机制,这套机制就是synchronized关键字,它包括两种用法:synchronized方法和synchronized块
同步方法:public synchronized void method(int args){}
synchronized方法控制对“对象”的访问,每个对象对应一把锁, 每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行
缺陷:若将一个大的方法声明为synchronized将会影响效率
方式二:Lock
- 从JDK 5.0开始,Java提供了更强大的线程同步机制一通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当
- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得L ock对象
- ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的ReentrantLock,可以显式加锁、释放锁。
synchronized与Lock的对比
- Lock是显式锁(手动开启和关闭锁,别忘记关闭锁) synchronized是隐式锁, 出了作用域自动释放
- Lock 只有代码块锁,synchronized有代码块锁和方法锁
- 使用L ock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
- 优先使用顺序: Lock >同步代码块(已经进入了方法体,分配了相应资源) >同步方法(在方法体之外)
线程通信
Java提供了几个方法解决线程之间的通信问题
方法名 | 作用 |
---|---|
wait() | 表示线程一直等待,直到其他线程通知,与sleep不同,会释放锁 |
wait(long timeout) | 指定等待的毫秒数 |
notify() | 唤醒一个处于等待状态的线程 |
notifyAll() | 唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先调度 |
注意:均是Object类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常legalMonitorStateException
注意: wait和 notify必须配合synchronized使用,wait方法释放锁,notify方法不释放锁
说明
- 1.wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。
- 2.wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。 否则,会出现IllegalMonitorStateException异常
- 3.wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中。
线程池
背景:经常创建和销毁,使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建和销毁,实现重复利用。
线程池的优势:
(1)降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
(2)提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
(3)提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
线程池参数:
- corePoolSize:核心池的大小
- maximumPoolSize:最大线程数
- keepAlive Time:线程没有任务时最多保持多长时间后会终止
线程池的使用:
- JDK 5.0起提供了线程池相关API: ExecutorService 和Executors
- ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
- void execute(Runnable command) :执行任务/命令,没有返回值,- -般用来执行Runnable
- Future submit(Callable task):执行任务,有返回值,一般 又来执行Callable
- void shutdown() :关闭连接池
- Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
例子:
public class ThreadPoolTest {
public static void main(String[] args) {
//1.创建服务,创建线程池
//newFixedThreadPool 参数为:线程池大小
ExecutorService service = Executors.newFixedThreadPool(10);
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
//2. 关闭链接
service.shutdown();
}
}
class MyThread implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
线程池的工作原理
注: 以上图片转载:http://t.csdn.cn/0ZcPa
相关面试题
1、synchronized 与 Lock的异同?
相同:二者都可以解决线程安全问题
不同:synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器,Lock需要手动的启动同步(lock(),同时结束同步也需要手动的实现(unlock())
2、sleep() 和 wait()的异同?
1.相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
2.不同点:1)两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait()
2)调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中
3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。