多线程
程序:一段静态的代码
进程:正在运行的一个程序,系统在运行时为每一个进程分配不同的内存区域。
线程:进程的进一步细化,与进程共享一个内存区域,但是有独立的运行栈和程序计数器。
单核CPU:假的多线程,一个线程执行一次,时间间隔较短,类似多车道,一个收费站。
多核CPU:更好的发挥多线程的效率。
一个java程序至少有三个线程:main()、gc()垃圾回收、异常处理线程。
并行:多个CPU执行多个任务
并发:一个CPU执行多个任务,如:秒杀,多人做同一件事
多线程的优点:
- 提高程序的相应,对于图形化界面,提高用户的体验
- 提高cpu的利用率。
- 将程序拆分成多个任务,方便代码的独立运行。
1、线程的创建和使用
线程的创建
方式一:继承于Thread
- 创建一个继承Thread类的子类
- 重写run()方法
- 创建Thread类的子类
- 通过子类对象调用start()方法
start()方法:①启动该线程 ②调用run()方法
注意:
- 直接调用run()方法并不会开启新的线程,是主线程正常运行run()方法。
- 不可以两次运行(同一对象调用2次start方法)相同的线程,必须重新new 线程对象。
方式二:实现Runnable接口
- 创建一个实现Runnable接口的类。
- 实现类实现run()方法。
- 创建实现类对象
- new Thread(实现类对象)
- 通过Thread类的对象调用 start()
Thread中的常用方法:
- start(),启用线程,调用run()
- run(),将需要多线程需要执行的方法写里面
- currentThread(),静态方法,返回当前执行代码的线程
- getName(),获取线程名
- setName(),设置线程名 / new Thread("线程名");
- yield(),释放当前cpu执行权
- join():在线程A中调用线程B的join方法,此时A就进入阻塞状态,直到B执行完,A才继续执行。
- sleep(long millitime):让当前线程睡眠一定的毫秒时间
- isAlive():判断当前线程是否存活
线程的调度:
- 线程的优先级:
MAX_PRIORITY :10
NORM_PRIORITY :5 默认优先级
MIN_PRIORITY :1
注意:高优先级的线程要抢占低优先级的线程cpu执行权,但是只是从概率上讲,并不是一定。
- 获取或设置当前线程的优先级
getPriority(): 获取
setPriority();设置
比较两种创建线程的方式:
开发中,优先选择实现Runnable接口的方式。
原因:
- 实现的方式没有类单继承的局限性
- 实现的方式更适合处理多个线程共有数据的情况
2、线程的生命周期
- 新建:调用start()
- 就绪:等待取得cpu资源
- 运行:
- 阻塞:①sleep() ②join() ③等待同步锁 ④wait()
- 死亡:①执行完 ②stop() ③error/exception且没有处理。
3、线程的同步
在java中,我们通过同步机制,来解决线程的安全问题。
同步的方式:
好处:解决了线程的安全问题
局限性:操作同步代码时,只有一个线程能操作,效率比较低。
方式一:同步代码块
synchronized(同步监视器){
//需要被同步的代码
}
说明:
- 需要操作共享数据的,就视为同步的代码。
- 同步监视器(锁),任何一个类的对象都可以充当锁。要求:多个线程共用一把锁
- 在实现Runnable接口的方式中,可以使用this充当锁。
- 在继承Thread的方式中,慎用this充当锁。可考虑当前对象
方式二:同步方法
操作共享数据的代码完整声明在一个方法中,可使用同步方法的方式。
实现Runnable接口的方式
public synchronize void a(){
}
继承Thread的方式
public static synchronize void a(){
}
注意:
- 在该方式中,没有显式的锁。
- 非静态的同步方法,默认锁是this。
- 静态的同步方法,琐是当前类本身。
改写单例模式中的懒汉式为线程安全:
class Bank{
private Bank(){}
private static Bank instance = null;
private static Bank getInstance(){
if (instance == null){
synchronized (Bank.class){
if (instance == null){
instance = new Bank();
}
}
}
return instance;
}
}
4、线程的死锁
理解:不同线程占用对方需要的资源不释放,处于僵持的状态,都在互相等待。
说明:出现死锁后程序并不会出现异常,只是所有线程都处于阻塞的状态。
解决方法:
- 专门的算法、原则
- 尽量减少同步资源的定义
- 避免嵌套同步
5、线程的同步--锁
Lock锁-----jdk5.0新增
-
实例化 ReentrantLock()对象
ReentrantLock lock = new ReentrantLock();
-
调用锁定方法lock.lock();
-
解锁lock.unlock();
synchronize 与 Lock 异同? (面试题)
-
相同:都可以解决线程安全问题
-
不同:synchronize 机制在执行完相应的同步代码后,自动释放锁。
Lock需要手动上锁(lock())和解锁(unLock());
使用顺序(建议/非必须)
Lock-->同步代码块-->同步方法
如何解决线程安全问题?有几种方式?(面试题)
三种:同步块,同步方法,锁Lock
6、线程的通信
线程通信的三个方法:
- wait() : 一旦执行此方法,当前线程就进入阻塞状态,并释放锁
- notify() : 执行此方法,唤醒被wait()的线程,如果有多个,就唤醒优先级高的
- notifyAll() : 执行此方法,唤醒全部wait()的线程
注意:
- 三个方法只能使用在同步代码块或同步方法中。
- 三个方法的调用者,必须是同步代码块或同步方法中的监视器(锁),否者会出现异常。
- 三个方法定义在Object中,不是Tread中。
实例:两个线程交替打印1-100
public class Test {
public static void main(String[] args) {
Number number = new Number();
Thread t1 = new Thread(number);
Thread t2 = new Thread(number);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
class Number implements Runnable{
int number = 1 ;
@Override
public void run() {
while (true){
synchronized (this) {
notify();
if (number<=100){
System.out.println(Thread.currentThread().getName()+":"+number);
number++;
}
else{
break;
}
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
sleep()和 wait()方法的异同? (面试题)
- 相同:都可以使线程阻塞
- 不同:
- 两者声明的位置不同,sleep()声明在Thread类,而wait()声明在Object类。
- sleep可以在任何位置使用,而wait()只能在同步代码块和同步方法中使用。
- 当两者都使用在同步代码块和同步方法中时,sleep()不会释放锁,wait()会释放锁。
例题生产者消费者
产品小于0个不能购买,大于0个可购买。
产品大于20不能再生产,不足20继续生产。
销售类
class Clerk{
private int num = 0;
public synchronized void produceProduct() {
if(num<20){
num++;
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":开始生产第"+num+"个产品");
notify();
}else{
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void customerProduct() {
if(num>0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":开始消费第"+num+"个产品");
num--;
notify();
}else{
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
生产者
class Producter extends Thread{
private Clerk clerk;
public Producter(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
while (true){
clerk.produceProduct();
}
}
}
消费者
class Customer extends Thread{
private Clerk clerk;
public Customer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
while (true){
clerk.customerProduct();
}
}
}
测试
public class ProductTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Producter p1 = new Producter(clerk);
Customer c1 = new Customer(clerk);
Customer c2 = new Customer(clerk);
p1.setName("生产者1");
c1.setName("消费者1");
c2.setName("消费者2");
p1.start();
c1.start();
c2.start();
}
}
7、新增线程的创建方式
方式一:实现Callable接口
JDK5.0 新增
与实现runnable接口相比更加强大:
- call方法中可以有返回值。
- call()可以抛出异常,被外面的操作捕获。
- Callable支持泛型。
步骤:
- 创建Callable的实现类
- 将要执行的操作写在Callable实现类的call方法中。
- 创建callable的实现类对象
- 创建FutureTask 对象 ,callable的实现类对象作为参数。
- new Thread(FutureTask 对象).start()开启线程;
- 可使用FutureTask 对象的get()方法,获取call()方法的返回值。
方式二:使用线程池
思路:提前建立好多个线程,放入线程池,随用随取。
好处:①提高响应速度②降低资源消耗
③便于线程管理:
- 线程池大小
- 线程数
- 没有任务的持续时间
步骤:
//1.提供指定数量的线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
//2.执行指定线程操作,需要提供实现runnable/callable类的对象
executorService.execute(new Number());
//executorService.submit(callable callable);
//3.关闭线程池
executorService.shutdown();
创建多线程有几种方式?(面试题)
4种:继承Tread
实现Runnable
实现Callable
线程池