Java学习-线程Thread

一、线程概述
进程(Processor):进程就是一段程序的执行过程;
线程(Thread):在一个程序中,这些独立运行的程序片段叫作线程,线程就是进程的一个任务,所以一个进程中至少有一个线程;
在这里插入图片描述

Java线程具有五中基本状态
1.新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();
2.就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
3.运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
4.阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
a.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
b.同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
c.其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
5.死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

线程类:
Thread类方法

  • static void sleep(long millis); 当前线程暂停xx毫秒,当前线程sleep的时候,有可能被停止,这时就会抛出异InterruptedException;
  • public final void join(long millis); 等待这个线程死亡的时间最多为millis毫秒,0的超时意味着永远等待;
  • void setPriority(int newPriority); 设置线程的优先级,优先级高的线程会有更大的几率获得CPU资源;
  • void setDaemon(boolean on); 将此线程标记为 daemon线程。 守护线程通常会被用来做日志,性能统计等工作;
  • static void yield(); 当前线程临时暂停;

二、创建多线程
1.创建新线程的第一种方式:继承Thread类,重写该类的run()方法

package ExtendsThread;
//1.继承Thread类
public class PrimeThread extends Thread{

	public PrimeThread(String string) {
		this.setName(string);
		return;
	}
	
	//2.重写run()方法
	@Override
	public void run() {
		for(int i=0;i<100;i++) {
			System.out.println(this.getName() + "prime线程运行");
		}
		super.run();
	}
}

package ExtendsThread;

public class demo {

	public static void main(String[] args) {
		//3.创建对象
		PrimeThread pt1 = new PrimeThread("pt1");
		PrimeThread pt2 = new PrimeThread("pt2");
		//4.开启线程
		pt1.start();
		pt2.start();
		//同时测试main线程
		for(int i=0;i<100;i++) {
			System.out.println("main线程运行");
		}
	}
}
console结果截取片段:
main线程运行
main线程运行
pt2prime线程运行
pt1prime线程运行
pt2prime线程运行
main线程运行
main线程运行

2.创建新线程的第二种方式:实现Runnable接口,并重写该接口的run()方法,创建Runnable实现类的实例,在创建Thread时作为参数传递,并启动;

package ImRunnable;
//1.实现Runnable接口
public class PrimeThread implements Runnable{
//2.重写run()方法
	@Override
	public void run() {
		for(int i=0;i<100;i++) {
			System.out.println("prime线程运行");
		}
	}
}

package ImRunnable;

public class demo {

	public static void main(String[] args) {
		//3.创建实现类的实例对象
		PrimeThread pt = new PrimeThread();
		//4.将实现类的实例作为参数传递进Thread来创建真实的线程对象
		Thread t = new Thread(pt);
		//5.开启线程
		t.start();

		//同时测试main线程
			for(int i=0;i<100;i++) {
				System.out.println("main线程运行");
			}
	}

}

思考:继承方式和实现方式的区别?

  • 耦合性分析
    1.继承方式:创建的子类线程中包含了运行的任务(run()方法),必须要开启此线程才能执行里面的任务,即线程和任务联系紧密耦合度高;
    2.实现方式:“任务”(实现类对象)作为“参数传递”进线程,此时需要执行什么任务就直接传进线程再开启,任务与线程无绑定的联系,耦合度低;
  • 扩展性分析
    1.继承方式:子类线程已经继承Thread类了,就无法继承其他类;
    2.实现方式:实现类只实现了接口,还可以继承其他类;
  • 源码分析
	 //Thread类源码
 private Runnable target;

   @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    } 
	//Runnable接口源码
  public interface Runnable {

    public abstract void run(); 
    }

3.匿名类的应用

package NiMing;

public class demo {

	public static void main(String[] args) {
		//继承方式的匿名内部类
		new Thread() {
			@Override
			public void run() {
				for(int i=0;i<50;i++) {
					System.out.println(i+"e线程运行");
				}
			}
		}.start();
		
		//实现接口方式的匿名内部类
		new Thread(	new Runnable() {
			@Override
			public void run() {
				for(int i=0;i<50;i++) {
					System.out.println(i+"r线程运行");
				}
			}
		}).start();
		
	}
}

三、解决线程安全的三种方式
多线程的同步问题指的是多个线程同时操作同一个对象的时候,可能导致的问题 (Concurrency 并发问题);
示例:

public class test {
	public int f = 1000;
	
	public void add() {
		f+=1;
		System.out.println("加法操作:" + f);
	}
	
	public void reduce() {
		f-=1;
		System.out.println("减法操作:" + f);
	}
}

public class demo {

	public static void main(String[] args) {
		//创建一个测试对象
		test ts = new test();
		//创建进程数组用来保存所有的进程对象
		Thread[] AddTh = new Thread[1000];
		Thread[] ReduceTh = new Thread[1000];
		
		//开启1000个加法进程
		for(int i=0;i<1000;i++) {
			Thread th1 = new Thread() {
				public void run() {
					ts.add();
					//每个进程执行时适当暂停,使同步问题的更容易出现
					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			};
			th1.start();
			//每开启一个进程,就保存此进程对象
			AddTh[i] = th1;
		}
		//开启1000个减法进程
		for(int i=0;i<1000;i++) {
			Thread th2 = new Thread() {
				public void run() {
					ts.reduce();
					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			};
			th2.start();
			ReduceTh[i] = th2;
		}
		//遍历进程数组,等待每一个加法线程结束
		for (Thread thread : AddTh) {
			try {
				thread.join(0);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		//遍历进程数组,等待每一个减法线程结束
		for (Thread thread : ReduceTh) {
			try {
				thread.join(0);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		//打印所有进程结束后,f的最终值
		System.out.println("最终结果" + ts.f);
	}
}
console结果:
减法操作:1001
减法操作:1000
减法操作:999
减法操作:998
最终结果:998

分析:

  • 打印的结果:加1000再减1000,按照我们的思路应该最终值还是1000,但是实际结果是998(这就是脏数据),表明同步问题的存在;
  • 为什么出现同步问题:比如当加法进程准备进行f = f+1(先f+1,再赋值给f)操作,可是加法(f+1)操作完成后没来得及赋新值,刚好其他加法进程插队又用旧的值先执行了f=f+1,两步加法操作结果都是一样;
  • 如何解决:保证某个进程在操作一个对象期间,只能被此进程占用进行操作直到操作完成,操作期间其他进程排队等待;

1.“外部”调用时使用synchronized同步
概念:synchronized关键字用于修饰同步对象,所有的对象,都可以作为同步对象
格式

//创建同步对象
Object synobj = new Object();
//线程占用对象
synchronized(synobj){
...//占用对象后执行的代码
}

作用:当前线程独占了对象synobj ,如果有其他线程试图占有对象synobj (同步对象),就会等待,直到当前线程释放对synobj的占用;
释放同步对象的方式: synchronized块自然结束,或者有异常抛出
示例

//上面代码按照如下仅做小改动,其他不变
synchronized(ts) {
	ts.add();
}
synchronized(ts) {
	ts.reduce();
}

2.“内部”直接使用synchronized修饰方法
格式

public synchronized void 方法名(){
...//占用对象后执行的代码
}

作用:synchronized修饰方法与第一种方式底层原理相同,其所对应的同步对象,就是this;
示例

public class test {
	public int f = 1000;
	
	public synchronized void add() {
			f+=1;
			System.out.println("加法操作:" + f);
	}
	
	public synchronized void reduce() {
			f-=1;
			System.out.println("减法操作:" + f);
	}
}
public class demo {
	public static void main(String[] args) {
	.....//内容和最初始的一样,不写了
	}
}
//以下的改动与synchronized修饰方法效果相同
public class test {
	public int f = 1000;
	
	public void add() {
		synchronized(this) {
			f+=1;
			System.out.println("加法操作:" + f);
		}
	}
	
	public void reduce() {
		synchronized(this) {
			f-=1;
			System.out.println("减法操作:" + f);
		}
	}
}

public class demo {
	public static void main(String[] args) {
	.....//内容和最初始的一样,不写了
	}
}

3.使用Lock
概念:Lock是一个接口,Lock锁是用于通过多个线程控制对共享资源的访问的工具;
格式

	//ReentrantLock 是Lock接口的常用实现类
       Lock l = new ReentrantLock();
       
       l.lock(); 
       try {
        ...// access the resource protected by this lock 
        } 
       finally { 
       l.unlock(); 
       } 

方法

  • void lock()
  • boolean tryLock()
  • boolean tryLock(long time, TimeUnit unit)
  • void unlock()

释放锁定对象的方式:lock却必须调用unlock方法进行手动释放,为了保证释放的执行,往往会把unlock() 放在finally中进行;
示例

四、线程交互
synchronized:使用synchronized方式进行线程交互,用到的是同步对象的wait,notify和notifyAll方法;
Lock:首先通过lock对象得到一个Condition对象,然后分别调用这个Condition对象的:await, signal,signalAll 方法;
sleep()方法是Thread类里面的,主要的意义就是让当前线程停止执行,让出cpu给其他的线程,但是不会释放对象锁资源以及监控的状态,当指定的时间到了之后又会自动恢复运行状态。
wait()方法是Object类里面的,主要的意义就是让线程放弃当前的对象的锁,进入等待此对象的等待锁定池,只有针对此对象调动notify方法后本线程才能够进入对象锁定池准备获取对象锁进入运行状态。

五、线程安全的类
概念:如果一个类,其方法都是有synchronized修饰的,那么该类就叫做线程安全的类
作用:同一时间,只有一个线程能够进入 这种类的一个实例 的去修改数据,进而保证了这个实例中的数据的安全(不会同时被多线程修改而变成脏数据);

六、Lock和synchronized的区别

  1. Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现,Lock是代码层面的实现。
  2. Lock可以选择性的获取锁,如果一段时间获取不到,可以放弃。synchronized不行,会一根筋一直获取下去。 借助Lock的这个特性,就能够规避死锁,synchronized必须通过谨慎和良好的设计,才能减少死锁的发生。
  3. synchronized在发生异常和同步块结束的时候,会自动释放锁。而Lock必须手动释放, 所以如果忘记了释放锁,一样会造成死锁。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值