——Java培训、Android培训、iOS培训、.Net培训、期待与您交流! ——-
注:视频来源,毕向东老师的 JAVA 基础视频。
一、线程和进程
1)进程:是一个在执行中的程序。每一个进程都有一个执行顺序,该顺序是一个执行路径,或者叫一个控制单元。
2)线程:就是进程中的一个独立的控制单元。线程在控制着进程的执行。
3) 理解:一个进程中,至少有一个线程。在JVM 启动的时候会有一个进程叫java.exe该进程中,至少有一个线程,在负责 java 程序的执行,而且这个线程,运行的代码存在于 main 方法中,该线程称之为主线程。其实,JVM 中更细节的说明了虚拟机不止一个线程,包含了一个主线程和一个负责控制垃圾回收机制的线程。通过API的查找,java已经提供了对线程这类事物的描述,就是 Thread 类。
4)多线程的随机性:因为多个线程都获取 cpu 的执行权。 cpu 执行到谁,谁就运行。但是要明确的是:在某一时刻,只能有一个程序在运行。(多核除外。)CPU 在做着快速的切换,以达到看上去是同时运行的效果。我们可以形象的把多线程的运行形容为在互相抢夺 CPU 的资源(执行权)我们可以形象的把多线程的运行形容为在互相抢夺 CPU 的资源(执行权)这就是多线程的特性,谁抢到谁执行。至于执行多长时间。CPU 说的算。
5)覆盖 run 方法的说明:Thread 类用于描述线程。该类就定义了一个功能,用于存储线程要运行的代码。该存储功能就是 run 方法。也就是说 Thread 类中 run 方法,用于存储线程需要运行的代码。所以说自定义线程,就是把代码放在run方法中。
6)线程都有自己的默认名称。Thread-编号,该编号从0开始。
currentThread:获取当前线程对象,这是一个静态的线程可以直接调用。
Ps:static Thread currentThread()
getName():获取线程的名称。
setName():设置线程名称,setName或者通过构造函数。
二、线程的生命周期
1)运行过程:
被创建: start()–>运行– sleep(time) –> 冻结(:放弃执行资格)
运行– sleep时间到 <– 冻结(:放弃执行资格)
运行– wait() –> 冻结(:放弃执行资格)
运行– notify() <– 冻结(:放弃执行资格)
2)消亡:stop() ,就是run方法中的结束。
3) 说明:没有资格执行的状态,冻结状态。当wait状态的时候,线程冻结了,没办法自动重启,这时候可以使用notify方法唤醒线程。
4)临时阻塞:具备运行资格,但是没有执行权。
线程的图解
三、创建线程的两种方式
总结两种方式的区别:
继承 Thread :线程代码存放在 Thread 子类 的 run 方法中。
实现 Runnable:线程代码存放在接口的子类的 run 方法中。
方式一:继承 Thread 类。
步骤:
1、定义类继承 Thread 。
2、复写 Thread 类中的 run 方法。目的:将自定义的代码存储在 run 方法中,让线程运行。
3、调用线程的 start 方法。该方法有两个作用,启动线程,调用run方法。
方法二:实现 Runnable 接口。
步骤:
1、定义类实现 Runnable 接口。
2、覆盖 Runnable 接口中的 Run 方法。将线程要运行的方法存放在该 run 方法中。
3、通过 Thread 类建立线程对象。
4、将 Runnable 接口的子类对象作为实际参数传递给 Thread 类的构造函数。
Ps:为什么要将 Runnable接口的子类对象传递给 Thread 的构造函数。因为,自定义的 run 方法所属的对象是Runnable 接口的子类对象,所以要让线程去执行指定对象的 run 方法。就必须明确该 run 方法所属的对象。
5、调用 Thread 的 start 方法开启线程,并调用 Runnable接口子类的 run 方法。
简单的卖票程序:
package fuxi;
/**
*
*@author XiaLei
*/
public class Day11Test {
public static void main(String[] args) {
ThreadTest t1 = new ThreadTest();
// ThreadTest t2 = new ThreadTest();
// ThreadTest t3 = new ThreadTest();
// ThreadTest t4 = new ThreadTest();
// t1.start();
// t2.start();
// t3.start();
// t4.start();被注释的是第一种方法,创建ThreadTest类继承Thread用其子类创建对象
/*
直接调用 run 方法,就没有开启线程。
start方法有两个作用:1.调用run方法2.开启线程
t1.run();
t2.run();
*/
new Thread(t1).start();
new Thread(t1).start();
new Thread(t1).start();
new Thread(t1).start(); //实现线程第二种方法,实现Runnable接口。
}
}
//class ThreadTest extends Thread{
// private int tickets = 100;
// public void run(){
//
// while(true){
// if (tickets>0){
// System.out.println(currentThread().getName()+"sale"+tickets--); //显示线程名及余票数
//
// }
// }
// }
//}
class ThreadTest implements Runnable{
private int tickets = 100;
public void run(){
while(true){
synchronized(this){ //给程序加同步,即锁 。
if (tickets>0){
try{Thread.sleep(10);}catch(Exception e){}//因为sleep方法有异常声明,所以这里要对其进行处理
System.out.println(Thread.currentThread().getName()+"sale"+tickets--);
}
}
}
}
}
四、多线程的安全问题
java 对于多线程的安全问题,提供了专业的解决方式。就是同步代码块。
好处:解决了多线程的安全问题。
弊端:多个线程需要判断锁,较为消耗资源。
1)最常用的线程使用方法:implements,就是用实现的方法写线程Thread。
2)运行时出现安全问题的原因:当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完。另一个线程参与了进来执行,导致了共享
数据的错误。
3)解决方法:在对多条操作共享数据的语句,只能让一个线程都执行完。在执行过程中,其它线程不可以参与执行。java 对于多线程的安全问题,提供了专业的解决方式。就是同步代码块。
4)同步的前提:
1、必须要有两个或者两个以上的线程才需要synchroized锁上。
2、必须是多个线程同时使用一个锁。
5)实现方式:
synchronized(对象){
需要被同步的代码//同步带共享操作的数据,如if的判断tick开始。
}
对象如同锁。持有锁的线程可以在同步中执行。没有持有锁的线程,即使获取了 CPU 的执行权,也进不去,因为没有获取锁。
package fuxi;
/**
* 需求:银行有一个金库。有两个储户分别存300员,每次存一百。存3次。
* 目的:该程序是否有安全问题:有的
* 如何找问题:
* 1、明确那些代码是多线程运行的代码。
* 2、明确共享数据。
* 3、明确多线程代码中哪些语句是操作共享数据的。
* 同步函数用的是哪一个锁呢?函数需要被对象调用。那么函数都有一个属性对象引用。就是this
* 所以同步函数使用的锁是this 。
*@author XiaLei
*/
public class Day11Test2 {
public static void main(String[] args) {
Cuz c1 = new Cuz();
new Thread(c1).start();
new Thread(c1).start();
}
}
class Bank{
private int sum;
public void add(int x){
synchronized(this){//也可以在函数上直接加锁
sum+=x;
System.out.println(Thread.currentThread().getName()+"=="+sum);
}
}
}
class Cuz implements Runnable{
private Bank b = new Bank();
public void run(){
for (int x = 0;x<3;x++){
b.add(100);
}
}
}
显示结果:
6)静态函数的同步方式
通过验证,发现不在是this。因为静态方法中也不可以定义this。静态进内存时,内存中没有本类对象,但是一定有该类对应的字节码文件对象。如:
类名.class 该对象的类型是Class
这就是静态函数所使用的锁。而静态的同步方法,使用的锁是该方法所在类的字节码文件对象。类名.class
经典示例:
package fuxi;
/**
*
*@author XiaLei
*/
public class day11Test11 {
public static void main(String[] args) {
}
}
//懒汉式单例设计模式
class Single{
private static Single s = null;
private Single(){}
public static Single getInstance(){
if (s==null)
synchronized(Single.class){
if (s==null){
s = new Single();
}
}
return s;
}
}
7)线程的死锁,同步中嵌套同步
package fuxi;
/**
* 思路:设计一个死锁程序,死锁就是同步里面嵌套同步,但是两个同步用的锁不同。
*@author XiaLei
*/
public class Day11Test3 {
public static void main(String[] args) {
DeadLock dl = new DeadLock(true);
DeadLock d2 = new DeadLock(false);
new Thread(dl).start();
new Thread(d2).start();
}
}
class Lock{
static Lock locka = new Lock();
static Lock lockb = new Lock();
}
class DeadLock implements Runnable{
boolean flag = true;
DeadLock (boolean flag){
this.flag = flag;
}
public void run(){
if (flag){
synchronized(Lock.locka){
System.out.println("if=="+"Lock.locka");
synchronized(Lock.lockb){
System.out.println("if=="+"Lock.lockb");
}
}
}
else{
synchronized(Lock.lockb){
System.out.println("else=="+"Lock.lockb");
synchronized(Lock.locka){
System.out.println("else=="+"Lock.locka");
}
}
}
}
}
打印结果:程序卡住,不能继续执行
五、线程间的通信之等待唤醒机制
1)线程间的通信:其实就是多个线程在同时操作一个资源,但是操作的动作不同。
2)等待唤醒机制:其实就是 wait 方法和 notify 方法的使用。
使用在同步中,因为要对持有监视器(锁)的线程操作。所以要使用在同步中,因为只有同步才具有锁。
3)为什么这些操作线程的方法要定义在Object类中呢?
因为这些方法在操作同步线程时,都必须要标识他们所操作线程的锁。只有同一个锁上的被等待线程,可以被同一个锁 notify 唤醒,不可以对不同锁中的线程唤醒。也就是说,等待和唤醒必须是同一个锁。而锁,可以是任意对象,所以可以被任意对象调用的方法定义在 Object 中。
等待唤醒机制示例:
package fuxi;
/**
*
*@author XiaLei
*/
public class Day12Test {
public static void main(String[] args) {
Res r = new Res();
new Thread(new Input(r)).start();
new Thread(new Output(r)).start();
}
}
class Res{
private String name;
private String sex;
private boolean flag = false;
public synchronized void set(String name,String sex){
if (flag)
try{this.wait();}catch(Exception e){}
this.name = name;
this.sex = sex;
flag = true;
this.notify();
}
public synchronized void out(){
if (!flag)
try{this.wait();}catch(Exception e){}
System.out.println(name+">>>"+sex);
flag = false;
this.notify();//唤醒等待
}
}
class Input implements Runnable{
private Res r;
Input(Res r){
this.r = r;
}
public void run(){
int x = 0;
while (true){
if (x==0)
r.set("jack", "man");
else
r.set("丽丽", "女女女女");
x=(x+1)%2;//让x变成1;
}
}
}
class Output implements Runnable{
private Res r;
Output(Res r){
this.r = r;
}
public void run(){
while(true){
r.out();
}
}
}
部分打印结果:
4)全部唤醒
对于多个生产者和消费者,就必须用 while + notifyAll(唤醒全部)。
1)为什么要用 while 判断标记呢?
原因:让被唤醒的线程再一次判断标记。
2)为什么定义 notifyAll ?
因为唤醒自己的同时还要唤醒对方的线程。只用 notify ,容易出现只唤醒本方线程的情况。导致程序中的所有线程都在等待。这也是比较有用的方法。
示例:
package fuxi;
/**
*
*@author XiaLei
*/
public class Day12Test1 {
public static void main(String[] args) {
Resouce r = new Resouce();
new Thread(new Producer(r)).start();
new Thread(new Producer(r)).start();
new Thread(new Consumer(r)).start();
new Thread(new Consumer(r)).start();
}
}
class Resouce{
private String name;
private int count = 1;
private boolean flag = false;
public synchronized void set(String name){
while(flag)
try{
wait();
}
catch(Exception e){
throw new RuntimeException();
}
this.name = name + count++;
System.out.println(Thread.currentThread().getName()+"...生产者......"+this.name);
flag = true;
notifyAll();
}
public synchronized void out(){
while(!flag)
try{
wait();
}
catch(Exception e){
throw new RuntimeException();
}
System.out.println(Thread.currentThread().getName()+"+消费者+"+this.name);
flag = false;
notifyAll();//全部唤醒
}
}
class Producer implements Runnable{
private Resouce r;
Producer(Resouce r){
this.r = r;
}
public void run(){
while(true){
r.set("汉堡");
}
}
}
class Consumer implements Runnable{
private Resouce r;
Consumer(Resouce r){
this.r = r;
}
public void run(){
while(true){
r.out();
}
}
}
部分打印结果:
5)JDK1.5中提供了多线程升级解决方案
将同步synchronized替换成显示的Lock操作。将Object中wait,notify,notifyAll,替换成了Condition对象。该Condition对象可以通过Lock锁进行获取,并支持多个相关的Condition对象。
升级解决方案的示例:
package fuxi;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
*
*@author XiaLei
*/
public class Day12Test2 {
public static void main(String[] args) {
Resouce r = new Resouce();
new Thread(new Producer(r)).start();
new Thread(new Producer(r)).start();
new Thread(new Consumer(r)).start();
new Thread(new Consumer(r)).start();
}
}
class Resouce{
private String name;
private int count = 1;
private boolean flag = false;
private ReentrantLock lock = new ReentrantLock();
//创建两Condition对象,分别来控制等待或唤醒本方和对方线程
private Condition condition_pro = lock.newCondition();
private Condition condition_con = lock.newCondition();
public void set(String name) throws InterruptedException{
lock.lock();
try{
while(flag)
condition_con.await();
this.name = name + count++;
System.out.println(Thread.currentThread().getName()+"...生产者......"+this.name);
flag = true;
condition_pro.signal();
}
finally{
lock.unlock();//解锁动作一定要执行
}
}
public void out() throws InterruptedException{
lock.lock();
try{
while(!flag)
condition_pro.await();
System.out.println(Thread.currentThread().getName()+"+消费者+"+this.name);
flag = false;
condition_con.signal();
}
finally{
lock.unlock();
}
}
}
class Producer implements Runnable{
private Resouce r;
Producer(Resouce r){
this.r = r;
}
public void run(){
while(true){
try {
r.set("汉堡");
} catch (InterruptedException e) {
}
}
}
}
class Consumer implements Runnable{
private Resouce r;
Consumer(Resouce r){
this.r = r;
}
public void run(){
while(true){
try {
r.out();
} catch (InterruptedException e) {
}
}
}
}
部分结果:
六、停止线程
1) JDK 1.5 后,停止线程的 stop 方法已经过时,那如何停止线程呢?
只有一种 run 方法结束。
2) 开启多线程运行,运行代码通常是循环结构。只要控制循环,就可以让 run 方法结束,也就是线程结束。
3)特殊情况:当线程处于冻结状态。就不会读取标记,那么线程就不会结束。
处理方法:强制中断线程:interrupt
当没有指定的方式让冻结的线程回复到运行的状态时,这时需要对冻结进行清除。强制让线程恢复到运行状态来,实际上就是获取运行资格。这样就可以操作标记,让线程结束。
守护线程,setDaemon(boolean b);
4) 扩展知识:
1、join方法
当A线程执行到了b线程的join()方法时,A线程就会等待,等B线程都执行完,A线程才会执行。(此时B和其他线程交替运行。)join可以用来临时加入线程执行。
2、setPriority()方法用来设置优先级
MAX_PRIORITY 最高优先级10
MIN_PRIORITY 最低优先级1
NORM_PRIORITY 分配给线程的默认优先级
3、yield()方法可以暂停当前线程,让其他线程执行。