- 1、什么是进程?什么是线程?他们有什么区别?
- 什么是进程?
- 什么是线程?
- 他们之间的关系、区别和优缺点?
- 程序计数器为什么是私有的?
- 虚拟机栈和本地方法栈为什么是私有的?
- 简单了解堆和方法区
- 2、为什么要使用多线程?
- 3、使用多线程可能带来什么问题?
- 怎么保证线程的安全?
- 什么是线程死锁?如何避免死锁?
- 产生死锁的四个条件:
- 如何预防和避免死锁:
- 4、说说sleep()方法和wait()方法的区别和共同点?
- 5、为什么我们调用start()方法时会执行run()方法,为什么不直接调用run方法?
- 6、谈一下对synchronized关键字的了解?
- 7、怎么使用synchronized关键字?
进程就是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序就是从一个进程创建,运行到消亡的过程。
在java中,当我们启动main方法,就是启动了一个jvm的进程。而main函数所在的线程就是这个进程中的一个线程,称为主线程。
线程与进程相似,但是线程是一个比进行更小的执行单位。一个进程在运行期间可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但是每个线程都有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或者切换线程时,负担比进程小得多,因此,线程也被称为轻量级进程。
他们之间的关系、区别和优缺点?从jvm虚拟机角度来看:
一个进程可以包含多个线程,同一个类中的线程可以共享进程的方法区和堆(jdk1.8之后称为元空间),每个线程都有自己的程序计数器、本地方法栈、虚拟机栈。
总之:线程是进程划分的更小的运行单位。它们最大的区别在于进程是独立的,而各线程不一定,同一进程中的线程可能会相互影响。线程的开销小,但是不利于资源的管理和保护,而进程正好相反。
程序计数器主要有两个作用:
(1)字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制。
(2)在多线程的情况下,记录当前线程的执行位置,从而当线程切换回来时能找到上次的执行位置。
注意:如果执行的是native方法,那么程序计数器记录的是undefined地址,只有执行的是java代码时,程序计数器才会记录下一条指令的地址。
程序计数器私有主要是为了在线程切换后能恢复到之前的执行位置。
虚拟机栈:
每个java方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完毕的过程,就对应着一个栈帧在java虚拟机中入栈出栈的过程。
本地方法栈:
和虚拟机栈发挥的功能很相似。但是区别是:虚拟机栈为虚拟机执行java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的native方法进行服务。在HotSpot虚拟机中和java虚拟机栈合二为一。
所有。为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。
堆:
是进程中最大的一块内存,主要用于存放新创建的对象(几乎所有的对象都在这里分配内存)。
方法区:
方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
(1)从计算机底层来说:
线程可以比作是轻量级的进程,是程序执行的最小单位,线程之间的切换和调度的成本远小于进程。另外,多核CPU时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。
(2)从互联网发展趋势来看:
现在的系统动不动就是百万级甚至是千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用多线程机制可以大大提高系统整体的并发能力以及性能。
可能会带来内存泄漏、死锁、线程不安全等问题。
我们可以把线程的内存分为主内存和工作内存。当有线程使用共享数据时,都是先从主内存中把数据拷贝到工作内存,使用完之后再写入到主内存,所有我们可以认为线程之间是通过共享内存的方式来实现通讯的。但是在多线程环境下,不用线程对同一份数据并发操作时,可能会产生不同线程中数据不一致的情况,这就是线程安全问题产生的原因。
一般要满足数据操作的3个基本特征:
原子性、可见性、有序性
原子性:确保线程之间互斥访问,同一时刻只能有一个线程对数据进行操作。可以使用atomic原子类或synchronized同步锁来满足。
可见性:当A线程修改了共享变量的内容时,能够立刻被其他的B线程知晓。即A线程中的共享变量副本修改后会立刻写入到主内存,然后其他线程读取时就能立刻得知数据的变化。可以使用volatile或synchronized来满足。
有序性:A线程观察其他线程中的指令执行顺序,由于该执行指令会进行重排序,所以观察结果一般会杂乱无序,一般遵循happens-before原则即可。
死锁:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。就比如线程A持有资源2,线程B持有资源1,它们都同时想申请对方的资源,这样这两个线程就会相互等待而进入死锁状态。
产生死锁的四个条件:(1)互斥条件:该资源任意一个时刻只能有一个线程占有。
(2)请求与保持条件:一个线程因请求资源而堵塞时,对已获得的资源保持不放。
(3)不剥夺条件:线程已获得的资源在未使用完之前不能被其他的线程强行剥夺,只有自己使用完毕只有才能释放资源。
(4)循环等待条件:若干线程之间形成了一种头尾相接的循环等待资源的关系。
如何预防死锁:
破坏死锁产生的必要条件即可:
(1)破坏请求与保持条件;
(2)破坏循环等待条件;
(3)破坏不剥夺条件;
如何避免死锁:
在资源分配时,借助与算法(比如银行家算法)对资源分配进行计算评估,是其进入安全状态。
(1)sleep()方法没有释放锁,wait()方法释放了锁。
(2)两者都可以暂停线程的执行
(3)wait()通常被用于线程间的交互、通信,sleep()通常用于暂停执行。
(4)wait()方法调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()或者notifyAll()方法。
sleep()方法执行完成后,线程会自动苏醒,或者可以使用wait(long timeout)超时后线程会自动苏醒。
new 一个 Thread,线程进入新建状态。调用start()方法,会启动一个线程并使线程进入就绪状态,当分到时间片之后就可以开始运行了。start()方法会执行线程的相应准备工作,然后自动执行run()方法,这是真正的多线程工作。但是,直接执行run方法,会把run方法当成main线程下一个普通方法去执行,并不会在某个线程中去执行,所以这不是多线程工作。
总结:调用start()方法可以启动线程并让线程进入就绪状态,直接执行run()方法不会以多线程的方式执行。
synchronized关键字解决的是多个线程访问资源的同步性,它可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
在java早期版本中,synchronized属于重量级锁,效率低下。
因为监视器锁是依赖于底层的操作系统的Mutex Lock实现的,java的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要较长的时间,时间成本相对较高。
在java6之后,java官方对从jvm层面对synchronized进行较大优化,所有现在的synchronized锁效率也优化的很好。jdk1.6对锁的实现引入了大量的优化,如自旋锁,适应性自旋锁,锁消除,锁粗化等技术来减少锁操作的开销。
主要有三种使用方式:
(1)修饰实例方法:作用于当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁。
synchronized void method(){
//业务代码
}
(2)修饰静态方法:也就是给当前类加锁,会作用于类的所有对象实例,进入同步代码前要获得当前class的锁。因为静态成员不属于任何一个实例对象,是类成员,所以如果一个线程A调用一个实例对象的非静态synchronized方法,而线程B需要调用这个实例对象所属类的静态synchronized方法,是允许的,不会发生互斥现象,因为访问静态synchronized方法所占用的锁是当前类的锁,而访问非静态synchronized方法占用的锁是当前实例对象锁。
synchronized static void method(){
//业务代码
}
(3)修饰代码块:指定加锁对象,对给定对象/类加锁。synchronized(this|object) 表示进入同步代码库前要获得给定对象的锁。synchronized(类.class)表示进入同步代码前要获得当前class锁。
synchronized(this) {
//业务代码
}
总结:synchronized关键字加到static静态方法和synchronized(class)代码块上都是给class类上锁。
synchronized关键字加到实例方法上是给对象实例上锁。
尽量不要使用synchronized(String a) ,因为JVM中,字符串常量池具有缓存功能。



