飞道的博客

java学习(10)-多线程中的一些概念和Thread地简单使用

410人阅读  评论(0)



说明

因为是个人复习java的总结,所以结构稍显杂乱,有些语句过于口语化.


并发和并行

并发是两个或多个事务在同一时间段发生
并行是两个或多个事务在同一时刻发生
也就是轮换和共同进行的区别


进程

就是应用程序进入到内存中,占用内存


线程

是进程中的一个执行单元,一个进程至少有一个线程.多个则是多线程程序


线程的调度

  分时调度,也就是平分cpu的占用时间
  抢占式调度,也就是根据优先级抢占线程运行程序,但是实际上是在高速切换各个程序的运行,只是优先级高会有更高几率执行到


创建线程类

首先要了解主线程,也就是main,所以main是作为java程序的入口
只有main时是单线程的程序, 创建子线程之后才是多线程
可以使用Thread类创建线程,每个线程都有优先级

具体的实现有两种方法:
  一种为申明一个类为Thread的子类并且重写run()方法,然后就可以通过创建这个类的对象调用start()方式,就可以启动新的线程.同时注意多次启动线程是非法的.
  另一种方式是实现Runnable接口,重写run()方法,然后再启动线程的时候先创建这个类的对象,再把这个类的对象作为参数用于创造Thread类对象,再调用start()
  第二种方法相对而言可以避免多继承的问题,同时减少了程序的耦合性,运行和设置线程分为两部分.

  从内存上看多线程其实是,在内存和cpu之间随线程的出现而开辟路径,并且根据优先级在cpu中抢占运行,所以对于相同优先级的线程就出现了线程运行的随机性,都有可能抢占内存运行.
  多线程的好处其实就是各个线程之间不会互相干扰等待,可以在一个线程运行较长时间程序片段的时候进行另外的操作.


Thread的方法

  可以通过getName()获取线程的名称,但是首先要先获取线程.currentThread()可以获取当前的线程.
  setName()可以设置线程的名称
  或者书写子类的带线程名称的构造方法,其中调用父类Thread的带线程名称的构造器,从而给线程命名.
  sleep()方法,就是暂停线程多少毫秒再执行


匿名内部类的方式实现线程的创建

  也就是说通过创建一个匿名内部类对象开启线程

new Thread(){
	@Override
	public void run(){
		重写run()
}
}.start();

  当然也可以采用接口的方式实现线程

new Thread(new Runnable(){
	@Override
	public void run(){
	重写run()方法
}
}).start();



线程安全问题

  其实就是多线程访问共享的内存,造成了数据修改,读取和更新不正确的问题
  因为多线程对于cpu是采用的抢占式运行,这就意味着很可能出现前一个线程还没完全运行对于共享区域的操作,后面的线程就抢占了,并且对共享内存操作了,这就导致出现了逻辑或者重复操作的问题.


同步代码块

代码格式如下:

synchronized(锁对象){
	可能出现线程安全问题的代码
}

  其实就是通过对一个对象进行加锁,然后实现多线程即使抢占了cpu也不能运行会造成冲突部分的代码
  也就是说一个线程运行,遇到同步代码块之后就会获取锁对象,即使这时候其他线程抢占了cpu,其他线程也不能获取锁对象,因为已经被占用,这样就不会触发线程安全问题的代码,只能等待前一个线程结束同步代码块运行,释放锁对象.


同步方法

  其实就是在方法定义的时候添加synchronized标记
  这种方式和同步代码块原理一样,只不过这里获取的锁对象是调用方法的对象,也就是说实现线程的类的对象,表现为this代表的对象.


静态同步方法

  静态同步方法和同步方法没有区别,只是需要注意这时获取的锁对象,是本类的class对象,也就是类.class
  具体对这个对象的说明会在反射中说明



synchronized实质上是在jvm中的操作,因此能够自动释放锁资源


Lock接口

  这个接口一般使用其实现类ReentrantLock来创建对象,然后多态表示为Lock索引,主要使用的是其中的lock()获取锁方法和unlock()释放锁方法.
  Lock区别于synchronized最主要的地方在于,Lock是实现类,需要手动去开启或者关闭锁,这就导致可能因为代码错误出现死锁问题.
  因此注意,一般书写方式如下

Lock l = new ReentrantLock();
try{
	l.lock();
}catch(){

}finally{
	l.unlock();
}

将释放资源放在finally中,有效避免出现资源不释放的问题


synchronized与Lock的区别

首先是一个为jvm管理,一个为类
其次synchronized不需要手动去操作,而Lock需要
再者synchronized是不能中断的除非出错或者运行完,而Lock是可以设置超时或者中断方法的.
另外synchronized是非公平锁,而Lock可以在构造方法传入参数选择是否为公平锁


关于锁

公平锁与非公平锁
  公平锁也就是多个锁按照申请锁时按照顺序来获取锁,即使刚好资源空,只要队列中有进程就得等待,因此吞吐效率较低
  非公平锁其实就是多个线程申请锁时先直接尝试获取锁,如果刚好可以获取,那就获取锁,否则去排队,吞吐效率较高,但是可能队列中的进程永远不能获取到资源


乐观锁与悲观锁
  乐观锁是假设最好的情况,也就是认为其他线程获取数据之后不会修改数据,所以获取数据的时候不加锁,但是在更新数据的时候会判断是否有其他线程在此期间更新数据.因此这种锁更适合读操作多的程序.
  其实乐观锁主要使用版本号机制和CAS算法实现,版本号机制就是对数据怎加版本号,每次修改都需比较版本号是否一致,成功一次则增加一次.
  CAS算法其实就是获取数据的时候保存一份,在更新的时候需要获取时数据和当前数据相同才会进行修改.
  但是也容易造成问题即期间多次操作但是数据仍和原来一样,却被认为没有改变,ABA问题.
同时反复运行,开销太大.

  悲观锁则是假设最坏的情况,认为其他线程获取数据之后都会修改数据,所以对于共享数据只允许同时存在一个线程进行操作,因此适宜在多写的程序使用.synchronized和ReentrantLock其实都是悲观锁


自旋锁和适应性锁
  自旋锁其实就是在线程访问资源失败,资源已被占用的时候,会循环判断能否占用锁,直到资源空闲.
  因此使用中大多会设置循环的次数,避免过长时间的循环,浪费太多资源
  适应性锁则是不需要设置,自动根据前面自旋锁的时间判断是否继续运行.


无锁,偏向锁,轻量级锁,重量级锁
  无锁就是不加锁
  偏向锁则是某线程一直获取某个资源的时候,偏向这个线程获取锁
  轻量级锁是指使用偏向锁时被其他线程访问,那么就会让其他线程自旋获取锁,提高性能,也就是意味着这个锁不会占用十分长时间
  重量级锁是指轻量级锁不满足,也就是资源占用时间过长,或者访问线程过多的时候,让等待的线程都进入休眠状态,减少不必要的运行时间.


可重入锁和非可重入锁
  其实就是对于同一个线程如果有多个需要获取锁的方法,那么在第一个方法获取锁之后,其他方法也能跟着获取锁,并且释放,最后第一个方法释放给其他线程.
  但是如果时非可重入锁就会导致当前线程占有锁后在等待其他方法完成,可是其他方法需要获取锁.这就造成了第一个方法不释放锁,其他方法不能获取锁,造成死锁的局面


独享锁和共享锁
  独享锁其实就是排他锁,在获取资源之后不允许其他锁获取这个资源
  共享锁则是能够多个线程获取同一个资源,具体可以在读写锁中理解,读锁可以多个线程都加到资源上,但是写锁不能有多个锁获取.


线程状态概述

总共六种:新建状态,运行状态,休眠状态,阻塞状态,等待状态,死亡状态.


休眠状态
进入休眠一段时间,然后自动运行.使用sleep()休眠


等待状态
  其实就是让线程进入等待,直到再次唤醒线程.
  但是对于等待状态使用wait()和notify()组合有个问题,就是无法唤醒指定线程,notify()会随机唤醒线程,而notifyAll则是唤醒所有线程.
  其实如果对wait()传入参数可以指定时间醒来.


等待唤醒机制

  也就是希望多个线程之间协调操作数据,避免线程之间争夺资源.
  实质上是通过上面的等待状态来达到协作的效果,wait(),notify(),notify All()
  需要注意wait和notify必须使用同一个锁对象调用,并且代码必须放在同步代码块或者同步方法中,锁对象可以是任意对象


线程池

如果并发的线程比较多,那么反复创建线程这个操作就很消耗资源
线程池其实就是一个存放了很多线程的容器,jdk5之后有自带的线程池


Executors类

  这是用来创建线程池的一个工厂类,其中有一个静态的方法newFixedThreadPool(int nThreads)用来创建可重用的静态线程池.nThreads线程数目
线程池的具体使用步骤如下:

  1. 使用工厂类生产一个线程池
  2. 创建一个类,实现Runnable接口重写run()定义线程任务
  3. 调用submit()方法,提交线程任务的对象到这个方法中
    然后线程池就会自动地选择线程进行任务,一个线程运行完后回到线程池等待下一个任务
  4. 关闭/销毁线程池的方法shutdown()但是不建议使用.


Lambda表达式

Runnable接口的实现代码冗余了,可以使用lambda表达式简化,代码如下

new Thread(() -> 线程任务).start();

  相当于对于省略了匿名内部类实现Runnable接口,而是直接重写接口的一个方法传入一些操作
lambda的标准格式如下:

(参数列表) -> {一些重写方法的代码}



下面是lambda在匿名内部类中的使用

  注意使用时必须具有接口,并且其中的抽象方法存在且唯一,并且需要符合上下文推断,也就是返回值和参数都是符合接口中抽象方法的类型.
  其实主要使用在调用某个方法需要使用到重写后接口中的方法时,如果使用实现类的方式传递,比较麻烦,或者使用匿名内部类实现也比较繁琐.这时可以直接使用lambda表达式,将需要重写的方法以lambda语句的方式代替匿名内部类对象.编译会重写接口中方法.

  比如Arrays里面的sort()方法有一种参数列表是传入集合和Comparator接口的实现并且重写compare(),这时就需要匿名内部类,因为重写内容少且只使用一次.那么匿名内部类对象的实现可以采用lambda表达式,也就是直接使用(Person o1, Person o2) -> {return o1.age-o2.age}代替匿名内部类对于compare()方法的重写.
  lambda表达式也可以有返回值,其实从其概念理解,直接重写了方法,对于方法的调动等没有区别.


lambda其实只要是能根据上下文推导出来的内容都可以省略书写

如以下几个方面:
  1.(参数裂变):括号中参数列表的数据类型,可以省略不写
  2.(参数列表):括号中的参数如果只有一个,那么类型和()都可以省略
  3.(一些代码):如果{}中只有一行,无论是否有返回值,都可以省略({}.return,分号).注意这些要一起省略



如有错误欢迎读者批评指正!!

图片截取自下面视频

https://www.bilibili.com/video/BV1A4411K7Gx?p=401


转载:https://blog.csdn.net/weixin_43913376/article/details/106077073
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场