基本概念
Java的内存模型
一个进程有一个方法区和一个堆;一个线程有一个虚拟机栈和一个程序计数器;多个线程共享进程的方法区和堆。
- 线程
- 若一个进程同一时间并行执行多个线程,就是支持多线程的
- 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小
- 一个进程中的多个线程共享相同的内存单元/内存地址空间它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患
- 一个Java程序至少有3个线程:main()主线程、gc()垃圾回收线程、异常处理线程
- 使用多线程的优点
- 提高应用程序的响应。对图形化界面更有意义,可增强用户体验
- 提高计算机系统CPU的利用率
- 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改
- 何时需要多线程
- 程序需要同时执行两个或多个任务
- 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等
- 需要一些后台运行的程序时
线程的创建和API
API中创建线程的两种方式
JDK1.5之前有两种方法:1.继承Thread类的方式;2.实现Runnable接口的方式
线程的执行流程:创建线程——启动线程(start())——调度run()(调度时机由OS的CPU调度决定)
注:一个线程对象只能调用一次start()方法启动,如果重复调用,则抛出“IllegalThreadStateException”异常
实现Runnable接口方式的好处:
- 避免了单继承的局限性;
- 多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。(比如卖票)
Thread类常用方法
- void start(): 启动线程,并执行对象的run()方法
- run(): 线程在被调度时执行的操作
- String getName(): 返回线程的名称
- void setName(String name):设置该线程名称
- static Thread currentThread(): 返回当前线程。在Thread子类中就是this,通常用于主线程和Runnable实现类
- static void yield():线程让步。暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程;若队列中没有同优先级的线程,忽略此方法( 此时这个线程也可以再去竞争资源,让步了、但没完全让步)
- join() :当某个程序执行流中调用其他线程的 join() 方法时,调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完为止。(会抛出InterruptedException异常)低优先级的线程也可以获得执行(与yield的不同)
- static void sleep(long millis):(指定时间:毫秒)
- 令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队
- 会抛出InterruptedException异常
- stop(): 强制线程生命期结束,不推荐使用
- boolean isAlive():返回boolean,判断线程是否还活着
Java中线程的调度和优先级
基本调度策略:时间片轮转调度
抢占式调度:高优先级的线程抢占CPU
Java的调度方法:同优先级线程组成先进先出队列(先到先服务),使用时间片策略;对高优先级,使用优先调度的抢占式策略
- Java线程的优先级等级:
- MAX_PRIORITY:10;
- MIN _PRIORITY:1
- NORM_PRIORITY:5
- getPriority() :返回当前线程优先值
- setPriority(int newPriority) :改变线程的优先级
注:1.线程创建时继承父线程的优先级;2.低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用
Java中线程的分类
Java中的线程有两类:1.守护线程;2.用户线程
两者唯一区别:判断JVM何时离开! 守护线程是用来服务用户线程的(荣国在start()方法前调用thread.setDaemon(true)可以把一个用户线程变成一个守护线程)
Java垃圾回收就是一个典型的守护线程,如果JVM中只剩下了守护线程,当前JVM就会退出(因为没有需要服务的用户线程了)
形象理解:兔死狗烹,鸟尽弓藏
线程的生命周期
线程在一个完整的生命周期中通常有五种状态:新建(NEW)、就绪(WAITING)、运行(RUNNABLE)、阻塞(BLOCKED)、死亡(TERMINATED)
线程的同步
多线程安全问题产生原因:当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。
解决办法:对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。(原子操作)
同步机制(synchronized)实现同步
Java对于多线程的安全问题提供的解决方法:同步机制
1.同步代码块:
synchronized(同步监视器(俗称锁)){
//需要被同步的代码;
}
注:任何一个类的对象,都可以充当锁
要求:多个线程必须要共用同一把锁(即用同一个对象,可以使用this/Window.class)
2.synchronized还可以放在方法声明中,表示整个方法为同步方法。操作共享数据的代码完整的声明在一个方法中。eg:public synchronized void show (String name) { }
//在同步方法中,同步监视器就是this,所以继承Thread类方式的新建线程对于同步方法最好设置为静态,不然就会是多个锁(因为一般都是新建不同的对象,不像实现Runnable接口一般是一个对象共享用)
//同步方法仍然涉及到同步监视器,只是不需要显式声明;
//非静态同步方法,同步监视器:this
//静态的同步方法,同步监视器:当前类本身
同步的好处:解决了线程的安全问题
同步的局限性:操作同步代码时,只能由一个线程参与,其他线程等待,相当于是一个单线程
同步可能导致的问题:死锁
单例模式懒汉式的线程安全
class Bank{
private static Bank instance = null;
private Bank(){}
public static Bank getInstance(){
//效率较差
// synchronized (Bank.class){
// if (instance==null) instance = new Bank();
// }
// return instance;
//方式二:效率较高,门口再立一个牌子,如果不为空就不用进入到线程同步的那段代码了
if(instance==null){
synchronized (Bank.class){
if (instance==null) instance = new Bank();
}
}
return instance;
}
}
Lock锁实现同步(JDK5.0新增)
Lock锁:显式定义同步锁对象来实现同步,同步锁使用Lock对象充当。java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
ReentrantLock类实现了Lock,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁
class Window implements Runnable{
private int ticket = 100;
//1.实例化ReentrantLock
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true){
try {
//2.调用锁定方法lock()
lock.lock();
if (ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":卖票,票号为:"+ticket);
ticket--;
}else{
break;
}
} finally {
//解锁
lock.unlock();
}
}
}
}
synchronized和lock方法的不同点:
1.synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器;而Lock需要手动的启动同步,同时手动的结束同步。
2.Lock只有代码块锁,synchronized有代码块锁和方法锁
3.使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
4.优先使用顺序:Lock——>同步代码块(已经进入方法体,分配了相应资源)——>同步方法(在方法体之外)
如何解决线程的安全问题?有几种方式
synchronized和lock