1.什么是线程
同一个时间段内,不同的任务模块可以并发进行,称为线程,是CPU的最小调度单位。
进程和线程的区别:
从资源分配的角度:
进程是资源分配的最小单位,一个进程的多个线程共享进程的资源。
从cpu调度的角度:
所有的程序都是以进程的形式运行在操作系统上,接受操作系统的资源分配,其中就包括CPU分配。
进程在执行的时候,会开辟多条线程,来处理不同类型的任务,CPU通过在线程上的调度实现进程间的调度。
从包含关系上:
进程包含线程,进程至少包含一条线程,线程最多属于一个进程。
进程切换和线程切换的成本也不同:
线程切换是不涉及虚拟地址空间的变化,而进程的切换需要改变虚拟的地址空间,操作系统本身会将不用的进程的一些依赖移除内存空间,给其他的应用程序让路。导致再切换到这个进程的时候,会发生很多的缺页错误,就是我调用的方法在内存中找不到,需要重新导入,这样的切换成本就很大了。
CPU按照一定的方法在线程之间切换,比如可以用condition的await和signal方法进行线程调度,也可以利用线程池来进行调度。按照一定的时间,或者是争抢资源的方式或者是ForkJoin的方式等。
并行和并发的区别
parallel是并行,concurrent是并发
在单处理器系统上,只可能存在并发,因为并行要求同一时间点,两个进程或线程分配到不同的cpu上执行,因而不争抢CPU资源。
并发是说多个线程或进程共享同一个时间段,在同一个CPU上,将cpu的运行时间分成若干时间片,一个时间片内只能有一个线程在执行。
比如开了100个消费者线程,设备是四核八线程的(四个物理核心,8个逻辑处理器(相当于一个人的左右手))java自动获取的Runtime.getRuntime().availableProcessors()是逻辑处理器的个数。
所以相当于100个消费者线程将在8个逻辑处理器上运行,那么同一时间在不同处理器上运行的是并行状态,而在同一个处理器上运行的,只能是并发状态。因为一个处理器同一时间只能执行一项任务。
2.如何开启一个新的线程
(1)写一个类继承Thread类
(2)写一个类实现Runnable接口,然后实例一个对象,new一个thread,调用start方法。
3.基本的线程同步——synchronized关键字
从synchronized关键字来说,锁定一个对象,当对象在执行一个同步代码块时,其他线程必须要等待该线程执行完毕释放对象的锁,其他的线程才能执行此代码块,保证了操作的原子性。线程执行完毕之后会将CPU缓存区的数据更新到主存。保证了可见性。
synchronized的几个用法
(1)锁定一个对象,或this对象
private Object oo=new Object();//堆内存里的一个引用
//synchronized的互斥性
sycnhronized(oo){
//要执行synchronized里面的代码,必须先申请拿到oo的锁。
//如果没有其他线程在执行,就可以成功拿到锁
//如果有其他线程正在执行同一段代码,这段线程只能在synchronized处等待oo锁的释放。然后拿到锁,再执行这段代码。
}
synchronized(this){
//用this来代替oo对象
//如果要执行这段代码,必须要等待this锁的释放。类似于厕所的门,锁住厕所自身。
}
(2)直接把synchronized放在方法的声明处
public synchronized void m(){
//这里synchronized锁定的也是this对象。
//如果新建其他的对象,来调用sychronized方法,其实是锁不住的。
}
(3)如果synchronized应用在一个静态方法上
public synchronized static void m(){
//因为静态方法是不需要对象来调用的,所以不能使用this关键字
//这种方法就和下面的synchronized(T.class)是一样的,使用反射的机制,获取了当前类的class对象
}
public static void m(){
synchronized(T.class){
//因为synchronized本质上是对对象上锁,用一个实例对象来调用方法,只有等对象的锁释放之后,其他的线程才能执行。
//和上面方法性质相同
}
}
(4)同步方法和非同步方法是否能同时执行:能,互不影响
public synchronized void m1(){
//同步方法执行过程中需要锁定当前对象
}
public void m2(){
//非同步方法执行过程中不需要锁定当前对象
}
T t=new T();
new Thread(()->t.m1()).start();
new Thread(()->t.m2()).start();//如果再有一个线程调用m1方法,就需要等待锁的释放,而新的线程去调用互不影响的m2方法,就不需要等待锁的释放。
//线程1的执行过程中,线程2 是可以执行的。
(5)写入加锁,但读取不加锁,容易产生脏读
public synchronized void write(){
//某些操作中对写入的方法加锁
}
public String READ(){
return getSomething;
//对读取的方法没有加锁,容易产生脏读,读的过程中,write还没有执行完,并没有最终写入的值。
//因为非同步方法是随时都可以执行的
//所以对于两个线程共享内存资源的时候,必须进行线程同步,读写都可以。
}
脏读可以用CopyOnWrite容器来实现。
写时复制容器,写的效率低,读的效率高。
写入时复制就是,对于要访问同一个内存的线程,如果不改变内存的值,那就直接一个指针指向这个内存地址,如果要修改这个内存的值,就copy一份副本给需要修改的调用者。修改的时候,有线程正在读取数据会读修改之前的数据,修改之后会立即改变引用,指向改变之后的容器。
当写入少,而读取多的时候适合用这个容器。
(6)同步方法调用另一个同步方法——重入锁
public synchronized void m1(){
//同步方法可以调用同步方法,synchronized本身是支持重入锁的。
//在执行m1的时候已经获得了对象的锁,调用同步方法m2,仍然是需要申请锁。
//因为是同一个线程内,synchronized锁是可以重入的,所以m2在可以执行。
m2();
}
public synchronized void m2(){
//同步方法m2
}
(7)子类调用父类的同步方法
public class T{
public synchronized void m(){
//
}
public static void main(String[] args){
new TT().m();
//new子类对象之前必须要new一个父类对象,但是synchronized锁定的是this对象也就是new TT()
}
}
public class TT extends T{
@Override
public synchronized void m(){
super.m();
}
}
(8)synchronized的优化
1)synchronized代码段越少越好,采用细粒度锁
2)避免改变sychronized锁住的对象的引用,改变了引用就锁不住了。
3)尽量不要锁定字符串常量
String s1="hello";
String s2="hello";
//实际上,s1s2指向常量池中的同一个内存,如果要锁定他们,实际锁定的是一个对象,可能产生死锁。
java锁只能锁住堆里的对象。