目录
一、进程
1.概念
2.为什么java不推荐使用进程
二、线程
1.概念
2.线程和进程之间的联系与区别
3.创建线程
4. Thread类的构造方法
5.Thread类的常见属性
6.同一个线程只能被start一次
7.start和run的区别
8.中断一个线程
9.等待线程
10.线程的状态
11.线程不安全问题
12.加锁解决线程不安全问题
正在运行的程序称为进程,进程是操作系统分配资源的基本单位。
进程在频繁被调用时,就会频繁创建和销毁,导致在资源的申请和释放上开销较大。
一个线程就是一个执行流,每个线程之间都可以按照顺序执行自己的代码,多个线程之间同时执行。线程是操作系统调度执行的基本单位。
(1)联系
一个进程至少包含一个线程。
(2)区别
①每个进程的内存是彼此独立的;同一进程的多个线程指向同一内存空间;②进程与进程之间互不影响;一个线程抛出异常,则同一个进程的线程也无法运行;同一进程的线程之间会互相干扰,引起线程安全问题。③进程是操作系统分配资源的基本单位;线程是操作系统调度执行的基本单位。
(1)继承Thread,重写run方法
class Thread1 extends Thread{
@Override
public void run() {
while (true){
System.out.println("hello thread1");
try {
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
public class test {
public static void main(String[] args) throws InterruptedException{
Thread1 t1=new Thread1();
t1.start();
while(true){
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
(2)实现Runnable接口,重写run方法
class Thread2 implements Runnable{
@Override
public void run() {
while(true){
System.out.println("hello Thread2");
try{
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
public class test2 {
public static void main(String[] args) throws InterruptedException{
Runnable runnable=new Thread2();
Thread t=new Thread(runnable);
t.start();
while (true){
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
(3)使用匿名内部类,继承Thread,重写run
public class test3 {
public static void main(String[] args) throws InterruptedException{
Thread t=new Thread(){
@Override
public void run(){
while (true){
System.out.println("hello thread");
try{
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
};
t.start();
while (true){
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
(4)使用匿名内部类,实现Runnable接口,重写run方法
public class test4 {
public static void main(String[] args) throws InterruptedException{
Thread t=new Thread(new Runnable(){
@Override
public void run() {
while (true){
System.out.println("hello thread");
try{
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
});
t.start();
while (true){
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
(5)使用lambda表达式
public class test5 {
public static void main(String[] args) throws InterruptedException{
Thread t=new Thread(()->{
while (true){
System.out.println("hello thread");
try{
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
});
t.start();
while (true){
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
(1)getId()、getName()
(2)isAlive()
(3)isDaemon()
t1是后台线程,主线程结束时,t1线程也结束
t1是前台线程,主线程结束时,t1线程仍然运行
前台线程的运行会阻止进程结束,后台线程的运行不会阻止进程的结束。
图1 start时,主线程main中的循环体也运行了;图2 run时,主线程main中的循环体未运行。start是在操作系统中创建出一个线程,和主线程兵分两路运行,而run是在主线程中调用了run方法,只要一个主线程,run无法挑出循环体导致主线程的循环体无法运行。
(1)引入标志位
public class test5 {
private static boolean flag=false; //和lambda变量捕捉有关
public static void main(String[] args) throws InterruptedException{
Thread t=new Thread(()->{
while (!flag){
System.out.println("hello thread");
try{
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
System.out.println("线程t终止");
});
t.start();
Thread.sleep(2000);
System.out.println("让线程t结束");
flag=true;
}
}
(2)Thread对象内置变量
public class test5 {
public static void main(String[] args) throws InterruptedException{
Thread t=new Thread(()->{
while (!Thread.currentThread().isInterrupted()){
//Thread.currentThread() 获取当前线程实例 哪个线程调用就是那个线程的实例
//isInterrupted() 使用Thread内部自带的内置变量代替flag
System.out.println("hello thread");
try{
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
System.out.println("线程t被提前唤醒!!!");
}
}
System.out.println("线程t终止");
});
t.start();
Thread.sleep(2000);
System.out.println("让线程t结束");
t.interrupt(); //相当于flag=true;
}
}
以上代码看着没有什么问题,会让线程t正常终止吗???
运行代码发现报异常,该异常来自于sleep被提前唤醒。如果线程t没有sleep会正常终止吗???
当线程t没有sleep时,终止线程不会报出异常,表明在终止线程t时,线程t刚好在sleep,线程t被唤醒后做两件事:①抛出InterruptedException异常;②标志位又设回原来的,线程继续执行。
为了解决以上问题,我们可以在sleep抛出InterruptedException异常后,之间break结束线程。
join方法,让一个线程等待另一个线程,等待线程被阻塞,一直到被等待线程完成run()方法
class Thread1 extends Thread{
@Override
public void run() {
for(int i=0;i<5;i++){
System.out.println("Thread1正在工作中");
try {
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
class Thread2 extends Thread{
@Override
public void run() {
for(int i=0;i<5;i++){
System.out.println("Thread2正在工作中");
try {
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
public class test {
public static void main(String[] args) throws InterruptedException{
Thread1 thread1=new Thread1();
Thread2 thread2=new Thread2();
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("Thread1和Thread2结束工作");
}
}
主线程中先是创建了Thread1线程并开始运行Thread1的run方法,然后创建了Thread2线程并开始运行Thread2的run方法,相当于兵分三路:主线程、Thread1线程、Thread2线程,随后Thread1.join()和Thread2.join(),主线程被阻塞,不参与cpu执行,只要等到Thread1和Thread2的run方法被执行完毕后才会接着执行主线程。
当修改代码为如下所示:
以上就为一个串行执行,先开始运行主线程,而后创建Thread1线程,兵分两路:主线程、Thread1线程,Thread1.join()后,主线程被阻塞,直到Thread1的run方法被执行完毕后,开始运行主线程,创建Thread2线程,兵分两路:主线程、Thread2线程,Thread2.join()后,主线程被阻塞,直到Thread2的run方法被执行完毕后,开始运行主线程。
join还有其他方法:
第二个join方法是等待的时间超过上限,等待线程就不再等待,接着运行。
(1)NEW Thread:对象创建好了,但是还没有调用start在系统中创建线程;
(2)TERMINATED Thread:对象仍然存在,但是对应的线程已执行完毕;
(3)RUNNABLE :就绪状态,表示这个线程正在cpu上执行或者随时准备去cpu上执行;
(4)TIMED_WAITING :指定时间的阻塞,到达一定时间后阻塞结束,例如:sleep()或者带时间的join();
(5)WAITING:不带时间的阻塞,需要满足一定条件后阻塞结束,不带时间的join()或者wait();
(6)BLOCKED:由于锁竞争引起的阻塞。
public class test2 {
private static int count=0;
public static void main(String[] args) throws InterruptedException{
Thread t1=new Thread(){
@Override
public void run() {
for(int i=0;i<50000;i++){
count++;
}
}
};
Thread t2=new Thread(){
@Override
public void run() {
for(int i=50000;i<=100000;i++){
count++;
}
}
};
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
大家认为上面代码的结果是多少?是100000吗???
结果是小于100000,若上述改为单线程计算结果会是什么???
发现是在单线程下计算结果正确,在多线程下出现bug,这种情况称为线程不安全。那么造成线程不安全的原因是什么呢???
回想cpu执行指令的步骤:先从内存中取出数据放入cpu寄存器中,再执行指令,最后把寄存器的值写回到内存中。count++相当于count+=1,如果一个线程执行上述部分自然不会有变数,可当多个线程执行时会存在变数。例如:可能第一个线程刚好执行cpu的第一步,还没执行完,第二个线程也紧跟着执行了,这时候会发现两个线程各加了一次,当结果只是加了一次。这是因为在第二个线程计算时我们没有保证第一个线程步骤全部计算完毕。
以上分析可知,出现线程不安全的(1)根本原因是:操作系统上的线程“抢占式执行”,随机调度;(2)直接原因是多线程实施的操作不是“原子的”;(3)代码结构:多个线程同时修改同一变量;(4)内存可见性问题;(5)指令重排序问题。
以下情况不会出现线程不安全问题:(1)一个线程修改一个变量;(2)多个线程读取同一个变量;(3)多个线程修改不同变量。
1.加锁的目的
将cpu的三个指令打包为一个整体,第一个线程运行完成后才能由下一个线程运行同一指令。
2.通过synchronized关键字加锁
(1)锁对象
在java中任何一个对象都可以作为锁对象,是因为加锁的功能是object的功能,而引用都继承于object类。
(2)加锁操作
①如果一个线程针对一个对象加上锁,而另一个线程针对同一个对象也加上锁;若第一个线程还未解锁,则第二个线程就会产生阻塞(BLOCKED),一直阻塞到第一个线程解锁为止。若是不同线程针对不同对象加锁,则不会有锁竞争和阻塞。
②代码实现
public class test3 {
private static int count=0;
public static void main(String[] args) throws InterruptedException{
Object locker=new Object();
Thread t=new Thread(()->{
for(int i=0;i<50000;i++){
synchronized (locker){
count++;
}
}
});
Thread t1=new Thread(()->{
for(int i=50000;i<100000;i++){
synchronized (locker){
count++;
}
}
});
t.start();
t1.start();
t.join();
t1.join();
System.out.println(count);
}
}
t1在执行count++时,不影响t2执行for循环括号里面的内容。
加锁之后确实会影响到多线程的执行效率,但即使这样也比单线程的执行效率快。
③加锁操作的一些混淆理解
同一个对象调用add方法,相当于不同线程对同一对象加锁;
class Locker{
public int count;
public void add(){
synchronized (this){
count++;
}
}
}
public class test4 {
public static void main(String[] args) throws InterruptedException{
Locker locker=new Locker();
Thread t1=new Thread(()->{
for (int i = 0; i < 50000; i++) {
locker.add();
}
});
Thread t2=new Thread(()->{
for (int i = 50000; i <100000 ; i++) {
locker.add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(locker.count);
}
}
将上述的this换成Locker.class也可以,Locker.class是类对象,一个类只有一个类对象,同一个类对象调用add方法,相当于不同线程对同一对象加锁;
可以直接将synchronized直接加到add方法上,该方法运行完成后才解锁。若是加到static方法上,则相当于加锁到类对象上。