多线程

多线程的实现

进程: 启动一个可执行文件就是一个进程
线程: 线程是进程内部所做的完成的任务

  1. 继承Thread 重写Run方法
    继承Thread启动线程
    实例化对象, 调用start方法

  2. 实现Runnable接口 重写Run方法
    实现Runnable接口启动线程
    实例化对象, new Thread(对象).start
    实现Runnable接口的类并没有start方法来启动线程, 只能通过new一个线程来启动, 或者采用匿名类

    new Thread(类名).start();
    
  3. 线程池实现多线程

  4. 匿名类实现多线程也很常用

  5. lambda表达式实现

run并不是启动启动线程, 只有start才是启动线程

常用方法

  1. 当前线程暂停
    Thread.sleep()
    Thread.sleep() 会抛出InterruptedException 中断异常
    单位是毫秒(一般为1000)

  2. 加入当前线程
    主线程会等当前线程结束才会继续运行(main即主线程).
    线程1.join()
    将线程1加入到主线程中, 只有等线程1结束后才会继续运行其他线程.

  3. 设置线程优先级
    线程1.setPriority();
    参数有Thread.MAX_PRIORITY最大, Thread.MIN_PRIORITY和Thread.NORM_PRIORITY。也可以自行设置数字等级。
    设置优先级必须在线程启动前设置

  4. 临时暂停
    使当前线程暂停, 让其他线程使用CPU资源
    线程1.yield();

  5. 线程状态
    得到该线程状态
    线程1.getState();
    线程的运行分为六个状态

    1. new状态:新建线程对象
    2. Runnable状态: Runnable有两个状态组成, 一个是Ready状态, 就绪状态, 任务处于CPU的等待队列中; 一个是Running状态, CPU运行任务
    3. teminated状态: 线程结束或中断
    4. timewaiting状态: 时间结束自动唤醒
    5. waiting状态: 睡眠(等待被唤醒)
    6. blocked状态:被锁定状态

关于InterruptedException异常, 这是我在简书上找到的一篇文章简书

守护线程

守护线程和用户线程:

	用户线程: 平常创建的普通线程
	守护线程: 用来服务用户线程, 不需要上层逻辑介入

设置守护线程(线程启动前设置)
线程1.setDaemon(true);

当主线程结束时,结束其余的子线程(守护线程)自动关闭,就免去了还要继续关闭子线程的麻烦。例:Java垃圾回收线程就是一个典型的守护线程;内存资源或者线程的管理,但是非守护线程也可以。

守护线程通常会被用来做日志,性能统计等工作。

注意事项:
(1) 当线程只剩下守护线程的时候,JVM就会退出;补充一点如果还有其他的任意一个用户线程还在,JVM就不会退出。

(2) 守护线程不能用于去访问固有资源,比如读写操作或者计算逻辑。因为它会在任何时候甚至在一个操作的中间发生中断。

并发和并行

并发:指的是任务数多余cpu核数,通过操作系统的各种任务调度算法,实现用多个任务“一起”执行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已)

并行:指的是任务数小于等于cpu核数,即任务真的是一起执行的(实际情况并不存在)

线程之间的执行是无序的

多线程资源共享问题

多线程修改数据时会发生一种情况
一个增量线程, 一个减量线程
增加线程进入, 修改数据, 但还没来得及修改数据时。此时减量线程运行了。
此时的数据还没有被增量修改, 就已经被减量所调用了。增量线程先修改数据, 减量线程后修改数据。最终结果就会有差异。这就叫脏数据

解决脏数据: 在当前线程访问数据期间, 其他线程不可以访问数据

  1. 当前线程获取到数据值,并进行运算
  2. 在运算期间,其他线程 试图来获取数据值,但是不被允许
  3. 当前线程运算结束,并成功修改数据值
  4. 其他线程,在当前线程结束后,才能访问数据值
  5. 其他线程 运算,并得到新的数据值

这时就要引入同步对象概念

同步对象

synchronized关键字

	Object C1 =new Object();
	synchronized (C1){
		//代码块
	}

synchronized表示当前线程独占对象C1。
在此期间, 如果有其他线程试图修改对象C1, 就会等待, 直到当前线程释放对C1的占用。
释放同步对象: synchronized块结束后自然释放, 或者异常抛出。

C1就是同步对象, 所有对象, 都可以被当做同步对象

synchronized的使用
同步对象在同一时间, 只能被一个线程占有(修改数据)。

	Object C1 =new Object();
	synchronized (C1){
			C1.alter();
		}
	//每次都用这种方式来同步对象太麻烦

有两种更为方便的用法

  1. 把类直接做为同步对象, 同步对象可以是所有对象, 线程会访问对象进而修改数据。
	//具体步骤
	public class WOW extends Thread{
	public int length = 0;
	
	public void shout(){
		synchronized(this){
			length +=1;
		}
	}

	public void run(){
		this.shout();
	}
	}

	public static void main(String[] args) {
		final WOW w1 = new WOW();
		int n = 1000;
		for(int i =0;i<n;i++){
			w1.run();
		}
		w1.start();
		try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
	}
  1. 在方法前使用synchronized修饰符
    效果与第一种方法相同
    在敲代码过程中, synchronized修饰的方法不如第一种方式好用, 个人使用观点
	public class WOW extends Thread{
	public int length = 0;
	
	public synchronized void shout(){
			length +=1;
	}

	public void run(){
		this.shout();
	}
	}

	//主方法同上

如果一个类的所有方法都被synchronized修饰, 那么该类就叫做线程安全的类。StringBuffer类就是线程安全的类。

同步对象在python中叫做互斥锁

死锁问题
线程1占有对象1, 等待对象2
线程2占有对象2, 等待对象1
此时就会出现死锁现象, 线程1等待对象2被释放, 线程2等待对象1被释放.
死锁是大忌!一定要合理使用synchronized

互斥锁
import java.util.concurrent.locks.*;

某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。

Lock是一个接口

Lock lock = new ReentrantLock();

lock()方法, 表示当前线程占用lock对象, 一旦锁定, 其他线程就不能占用了.

与synchronized不同的地方在于, synchronized块结束后会自动释放对同步对象的占用. 互斥锁必须调用unlock方法手动释放, 通常unlock方法会放在finally中.

trylock方法
在指定时间范围内试图占用, 成功占用就进行修改, 如果占用不成功, 就放弃.
因为有存在占用不成功的情况, 所有解除锁定就要进行判断.

		locked = lock.tryLock(1,TimeUnit.SECONDS);
        if(locked){
   				Thread.sleep(5000);
                    }
        else{
        }
线程间的交互

使用synchronized情况下
this.wait() 将占有this的线程等待, 并临时释放占有
this.notify() 使等待this的线程苏醒
两个方法都是放在线程会调用的方法中
this.wait()使用场景: 在某特定情况下, 将线程1占有的对象暂时释放, (线程1进入休眠状态)
对象被释放, 其他线程占有对象
其他线程操作结束后末尾加上this.notify(), 唤醒线程1.

public synchronized void Amethod(){
	if(...){
		this.wait(); //用try包裹
	}
	...
}

public synchronized void Bmethod(){
	...
	...
	this.notify();
}

关于wait和notify方法
这两种方法并不是thread线程的方法, 是Object的方法.
因为所有的Object都可以被用来作为同步对象,所以准确的讲,wait和notify是同步对象上的方法.

wait()的意思是: 让占用了这个同步对象的线程,临时释放当前的占用,并且等待。 所以调用wait是有前提条件的,一定是在synchronized块里,否则就会出错。

notify() 的意思是,通知一个等待在这个同步对象上的线程,你可以苏醒过来了,有机会重新占用当前对象了。

notifyAll() 的意思是,通知所有的等待在这个同步对象上的线程,你们可以苏醒过来了,有机会重新占用当前对象了。

使用Lock情况下
Lock对象得到一个Condition对象, 然后调用Condition对象的await, signal, signalAll方法. 和synchronized类似

	Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();
    condition.await();
    condition.signal();
    condition.signalAll();
总结
  1. Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现,Lock是代码层面的实现。

  2. Lock可以选择性的获取锁,如果一段时间获取不到,可以放弃。synchronized不行,会一根筋一直获取下去。 借助Lock的这个特性,就能够规避死锁,synchronized必须通过谨慎和良好的设计,才能减少死锁的发生。

  3. synchronized在发生异常和同步块结束的时候,会自动释放锁。而Lock必须手动释放, 所以如果忘记了释放锁,一样会造成死锁。

线程池

线程池就是一个含有多个线程的容器, 将任务抛给线程池后, 线程池中的线程就会执行这个任务. 当有多个任务时就会将多个线程唤醒. 整个过程中都不需要创建新的线程, 而线程池中的线程也会循环使用.

使用线程池之前先自己编写一个线程池了解其原理, 再使用Java自带的线程池

线程池类ThreadPoolExecutor在包java.util.concurrent下

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

ThreadPoolExecutor有五个参数

ThreadPoolExecutor threadpool = new ThreadPoolExecutor(10,20,60,TimeUnit.SECONDS,new LinkedBlockingQueue<Runnable>());

第一个参数表示线程池的初始线程个数, 10个线程
第二个参数表示如果初始线程不够用了, 额外增加的线程数. 当10个线程不够用时, 最多会自动增加10个线程, 即上限为20个.
第三个参数与第四个参数组合使用, 表示经过60秒后, 空闲的线程没有任务时就会被收回线程池.
第五个参数是存放任务的集合.

threadpool.execute(参数);

参数是要放入线程池的任务, 且必须是继承了Thread类或实现了Runnable接口.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值