一、线程概述
进程(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的区别
- Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现,Lock是代码层面的实现。
- Lock可以选择性的获取锁,如果一段时间获取不到,可以放弃。synchronized不行,会一根筋一直获取下去。 借助Lock的这个特性,就能够规避死锁,synchronized必须通过谨慎和良好的设计,才能减少死锁的发生。
- synchronized在发生异常和同步块结束的时候,会自动释放锁。而Lock必须手动释放, 所以如果忘记了释放锁,一样会造成死锁。