java基础知识之多线程
1.多线程的概述
进程:简而言之就是一个正在进行的程序,每一个进程执行都有一个执行的顺序,该顺序是一个执行的路径,或者叫一个控制单元。
线程:就是进程中的一个独立的控制单元。线程控制着程序的进行,一个进程中至少有一个线程。
多线程:简而言之就是一个进程中有多个线程在控制着程序的运行。
例如:jvm启动的时候会有一个进程java.exe,该进程中至少有一个线程负责java程序的运行,
而且这个线程运行的代码存在于main方法中,该线程称之为主线程,但是jvm启动的时候不止
一个线程,还有负责垃圾回收机制的线程。
2.创建线程---继承Thread类
如何在代码中自定义一个线程
通过对api帮助文档的查找,java已经提供了对线程这类事物的描述,就是Thread类。
创建线程的第一种方式:继承Thread类
1.定义 类继承Tread
2.复写Tread类中的run方法
3.调用线程的start方法,该方法有两个作用:启动线程,调用run方法。
例如:
class Demo extends Thread
{
//复写Tread类中的run方法
public void run() {
for(int i=0;i<50;i++){
System.out.println("Demo run");
}
}
}
class DemoTest{
public static void main(String [] args){
Demo d=new Demo();//创建好一个线程
d.start();//.调用线程的start方法,该方法有两个作用:启动线程,调用run方法
for(int i=0;i<50;i++){
System.out.println("Hello word!");
}
}
}
输出的结果是50个hello word!和50个Demo run,但是每一次输出的结果的顺序都不一样。
分析结果:从结果可以体现出多线程的一个特性:随机性。这是因为多个线程都在获取cpu的执行权,当cpu执行到该线程时就执行,在某一个时间段,只能有一个程序在运行,(多核除外),cpu在做着快速的切换,以达到看上去是同时运行的结果,我们可以形象的把多线程的运行理解为抢夺xpu的执行权。
3.创建线程---run和start之间的区别
run方法:run方法是Tread定义的一个功能,用于存储线程要运行的代码,复写run方法的目的在于将自定义
的代码存储在run方法中,让线程运行。
start方法:开启线程并执行该线程的run方法 。
注:当建立好线程的对象之后,应该调用的是start方法,而不是run方法,因为run方法仅仅是对象调用方法
而线程创建了之后并没有运行。
4.线程运行状态
1、新建状态(New):新创建了一个线程对象。
2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
(一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。
(二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
(三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
解析:
1.当我们new了这个对象后,线程就进入了初始状态;
2、当该对象调用了start()方法,就进入可运行状态;
3、进入可运行状态后,当该对象被操作系统选中,获得CPU时间片就会进入运行状态;
4、进入运行状态后情况就比较复杂了
4.1、run()方法或main()方法结束后,线程就进入终止状态;
4.2、当线程调用了自身的sleep()方法或其他线程的join()方法,就会进入阻塞状态(该状态既停止当前线程,但并不释放所占有的资源)。当sleep()结束或join()结束后,该线程进入可运行状态,继续等待OS分配时间片;
4.3、线程调用了yield()方法,意思是放弃当前获得的CPU时间片,回到可运行状态,这时与其他进程处于同等竞争状态,OS有可能会接着又让这个进程进入运行状态;
4.4、当线程刚进入可运行状态(注意,还没运行),发现将要调用的资源被synchroniza(同步),获取不到锁标记,将会立即进入锁池状态,等待获取锁标记(这时的锁池里也许已经有了其他线程在等待获取锁标记,这时它们处于队列状态,既先到先得),一旦线程获得锁标记后,就转入可运行状态,等待OS分配CPU时间片;
4.5、当线程调用wait()方法后会进入等待队列(进入这个状态会释放所占有的所有资源,与阻塞状态不同),进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能被唤醒(由于notify()只是唤醒一个线程,但我们由不能确定具体唤醒的是哪一个线程,也许我们需要唤醒的线程不能够被唤醒,因此在实际使用时,一般都用notifyAll()方法,唤醒有所线程),线程被唤醒后会进入锁池,等待获取锁标记。
5.获取线程对象以及名称
线程都有自己默认的名称:Thread---编号 该编号从0开始(在不赋值的情况下)
举例说明:
class Demo extends Thread
{
Demo(String name){
super(name);//---继承了Tread类中的构造方法Tread(String name);
}
//复写Tread类中的run方法
public void run() {
System.out.println(this.getName()+" Demo run");
}
}
class DemoTest{
public static void main(String [] args){
Demo d1=new Demo("one");//创建好一个线程并设置线程的名称为one
Demo d2=new Demo("two");//创建好一个线程并设置线程的名称为two
d1.start();//.调用线程的start方法,该方法有两个作用:启动线程,调用run方法
d2.start();
}
}
输出的结果是:one Demo run和two Demo run
代码中的this等同于Thread.currentTread()方法,作用是返回对当前正在执行的线程对象的引用。
getName():获取线程的名称
设置线程的名称可以使用setName()和构造函数。
6.创建线程---实现Runable接口
创建线程的第二种方式:实现Runable接口
创建步骤:
.定义类实现Runnable接口
.覆盖Runnable接口中的run方法,将线程要运行的代码存放在该run方法中
.通过Thread类建立线程对象
.将Runnable接口的子类对象作为实际参数传递给Tread类的构造函数(将Runnable接口的子类对象传递给
Tread的构造函数时因为自定义的run方法所属的对象时Runnable接口的子类对象,所以要让线程去执行
指定的对象的run方法,就必须明确该run方法所属的对象)
.调用Tread类的start()方法开启线程并调用Runnable接口子类的run方法。
举例说明:售票
//定义类实现Runnable接口
class Ticket impliments Runnable
{
private int tick =10;
//覆盖Runnable接口中的run方法
public void run(){
while(true){
if(tick>0){
System.out.println(Thread.currentTread().getName()+"--------sale:"+tick--);
}
}
}
}
class TicketTest{
public static void main(String[] args){
Ticket t=new Ticket();//通过Tread类建立线程对象
Thread t1=new Thread(t);//将Runnable接口的子类对象作为实际参数传递给Tread类的构造函数
Thread t2=new Thread(t);
Thread t3=new Thread(t);
t1.start();//调用Tread类的start()方法开启线程并调用Runnable接口子类的run方法
t2.start();
t3.start();
}
}
输出结果为1至10的数字(随机),一共有3个线程在运行。
使用实现Runnable接口的方式创建多线程的好处:避免了单继承的局限性。
创建线程两种方式的区别:
继承Thread:线程代码存放在Thread子类run方法中。
实现Runnable :线程代码存放在接口的子类的run方法中。
7.多线程的安全问题
安全问题:
上述例子极有可能会产生安全问题,输出结果可能为负数,问题产生的原因是因为当多条语句在操作同一个对象共享数据时
一个线程对多条语句只执行了一部分,还没有执行完,另一个线程就参与进来执行,导致了共享数据的错误。
解决思路:
对多条操作共享数据的语句,只能让一个线程都执行完毕,在执行过程中,其他线程不可以参加运行。
解决方式:使用同步代码块
格式:synchronized(对象)
{
需要被同步的代码块
}
对象如同锁,持有锁的线程可以再同步中执行,没有持有锁的线程及时获取了cpu 的执行权,也进不去,因为没有获取锁。
例子修改如下:
//定义类实现Runnable接口
class Ticket impliments Runnable
{
private int tick =10;
Object obj=new Object();
//覆盖Runnable接口中的run方法
public void run(){
while(true){
//使用synchronized(对象)方法将需要同步的代码块包起来
synchronized(obj){
if(tick>0){
try{Tread.sleep(10);}catch(Exception e){}//异常处理
System.out.println(Thread.currentTread().getName()+"--------sale:"+tick--);
}
}
}
}
}
class TicketTest{
public static void main(String[] args){
Ticket t=new Ticket();//通过Tread类建立线程对象
Thread t1=new Thread(t);//将Runnable接口的子类对象作为实际参数传递给Tread类的构造函数
Thread t2=new Thread(t);
Thread t3=new Thread(t);
t1.start();//调用Tread类的start()方法开启线程并调用Runnable接口子类的run方法
t2.start();
t3.start();
}
}
同步的前提:
.必须要有两个或者两个以上的线程
.必须是多个线程使用同一个锁
.必须保证同步中只能有一个线程在运行
同步的好处:解决了多线程的安全问题
同步的弊端:多个线程需要判断锁,较为消耗资源。
如何找出程序中是否有安全问题:
1.明确哪些代码是多线程的运行代码
2.明确哪些是共享数据
3.明确多线程运行代码中哪些语句是操作共享数据的。
8.多线程---同步函数
同步函数:就是对某个函数加锁,在一个对象访问这个方法的时候,其他方法如果得到权限,要访问这个函数(这样可能会造成数据库的错误值),就碰到所的检测,因为已经有对象访问这个方法了,所以不允许其他对象访问这个函数
银行存款例子:
class Bank{
private int balance;
publicsynchronized void add(int n){
sum=sum+n;
try{Thread.sleep(10);}catch(Exception e){}
System.out.println("sum"+sum);
}
}
class Cus implements Runnable{
private Bank b=new Bank();
public void run(){
for(int i=0;i<3;i++){
b.add(100);
}
}
}
class BankTest{
public static void main(String []args){
Cus cus=new Cus();
Tread t1=new Thread(cus);
Tread t2=new Thread(cus);
t1.start();
t2.start();
}
}
输出结果是:100 200 300 400 500 600
同步函数的锁是this
函数需要被对象调用,那么函数都有一个所属对象引用,就是this,所以同步函数使用的锁是this.
举例说明:
//定义类实现Runnable接口
class Ticket impliments Runnable
{
private int tick =10;
//覆盖Runnable接口中的run方法
public void run(){
while(true){
this.show();
}
}
public synchronized void show(){
if(tick>0){
try{Tread.sleep(10);}catch(Exception e){} //异常处理
System.out.println(Thread.currentTread().getName()+"--------sale:"+tick--);
}
}
}
class TicketTest{
public static void main(String[] args){
Ticket t=new Ticket(); //通过Tread类建立线程对象
Thread t1=new Thread(t); //将Runnable接口的子类对象作为实际参数传递给Tread类的构造函数
Thread t2=new Thread(t);
Thread t3=new Thread(t);
t1.start(); //调用Tread类的start()方法开启线程并调用Runnable接口子类的run方法
t2.start();
t3.start();
}
}
static修饰的同步函数的锁是Class对象
如果函数被静态修饰后,函数的锁就不是this了,因为静态函数中不可以定义this.
(静态进内存时,在内存中并没有本类的对象,但是一定有该类对应的字节码文件对象,静态
的同步方法使用的是锁是该方法所在类的字节码文件对象即类名.class,该对象的类型是class)
举例说明:
//定义类实现Runnable接口
class Ticket impliments Runnable
{
//静态修饰的变量
private
static int tick =10;
//覆盖Runnable接口中的run方法
public void run(){
while(true){
show();
}
}
//静态修饰的同步函数
public
static synchronized void show(){
if(tick>0){
try{Tread.sleep(10);}catch(Exception e){} //异常处理
System.out.println(Thread.currentTread().getName()+"--------sale:"+tick--);
}
}
}
class TicketTest{
public static void main(String[] args){
Ticket t=new Ticket(); //通过Tread类建立线程对象
Thread t1=new Thread(t); //将Runnable接口的子类对象作为实际参数传递给Tread类的构造函数
Thread t2=new Thread(t);
Thread t3=new Thread(t);
t1.start(); //调用Tread类的start()方法开启线程并调用Runnable接口子类的run方法
t2.start();
t3.start();
}
}
9.多线程---单例设计模式
懒汉式单例设计模式
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;
}
}
懒汉式用于实例的延迟加载 ,但是当多个线程访问时会发生安全问题,解决的办法是加同步进行,同步代码块和
同步函数都可以解决,但效率比较低效,因为增加了线程访问判断锁的次数,所以使用双重判断的形式来解决效率的问题。
加同步的锁是该类所属的字节码文件对象。
10.多线程---死锁
在编程过程中应尽可能的避免出现死锁的情况,出现死锁的情况往往是因为在同步中嵌套同步
例如:
//定义类实现Runnable接口
class Test implements Runnable{
private boolean flag;
Test(boolean flag){
this flag=flag;
}
public voic run(){
if(flag){
synchronized(Lock.obj1){
System.out.println("if-----obj1");
synchronized(Lock.obj2){
System.out.println("if-----obj2");
}
}
}
else{
synchronized(Lock.obj2){
System.out.println("else-----obj2");
synchronized(Lock.obj2){
System.out.println("else-----obj1");
}
}
}
}
}
//定义的是两个锁的类
class Lock{
static Object obj1=new Object();
static Object obj2=new Object();
}
//测试代码
class main{
public static void main(String []args){
Thread t1=new Thread(new Test(true));
Thread t2=new Thread(new Test(false));
t1.start();
t2.start();
}
}
输出结果是:if-----obj1和else-----obj2(两者先后顺序不定)
因为程序出现了死锁的现象,始终没有办法执行完代码。
11.线程间通信---解决安全问题
线程间通信:其实就是多个线程在操作同一个资源,但是操作的动作不同,
例子:
//定义操作的类
class Res{
String name;
String sex;
}
//定义类实现Runnable接口(输入即赋值操作)
class Input implements Runnable{
private Res rs;
Input(Res rs){
this.rs=rs;
}
public void run(){
int x=0;
while(true){
//线程同步解决安全问题
synchronized(rs){
if(x==0){
rs.name="张三";
rs.sex="男";
}else{
rs.name="丽丽";
rs.sex="女";
}
x=(x+1)%2; //相当于布尔类型,是一个死循环
}
}
}
}
//定义类实现Runnable接口(输出即取值操作)
class Output implements Runnable{
private Res rs;
Output(Res rs){
this.rs=rs;
}
public void run(){
while(true){
//线程同步解决安全问题
synchronized(rs){
System.out.println(rs.name+"-------------"+sex);
}
}
}
}
//定义测试类
class InputOutputDemo{
public static void main(String []args){
//创建操作资源对象
Res rs=new Res();
//建立线程对象
Input intput=new Input(rs);
Output outtput=new Output(rs);
//将Runnable接口的子类对象作为实际参数传递给Tread类的构造函数
Thread t1=new Thread(intput);
Thread t2=new Thread(output);
t1.start(); //启用线程
t2.start();
}
}
输出的结果:
一连串的张三 男和一连串的丽丽 女
Input和Output两个线程操作的是同一资源Res,但是操作的动作即功能不一样。
12.线程间通信---等待唤醒机制
wait()-----等待
notify()----唤醒
notifyAll----唤醒全部
(都使用在同步中,因为要对持有监视器(锁)的线程操作,所以要使用在同步中,因为只有在同步中才有锁,都定义在object类中)
关于以上方法都定义在Object类中的原因:
因为这些方法在操作同步线程时,都必须要标识它们所操作线程持有的锁,
只有同一个锁上的被等待线程可以被同一个锁上的notify唤醒,不可以对不同锁中的线程进行唤醒,即等待和唤醒必须
是同一个锁,而锁可以使任意的对象,所以可以被任意对象调用的方法定义在Object类中。
通过11的例子的输出结果可以得知这不是我们想要的结果,应该是存入一条数据后打印该数据,再存入下一条数据,这时就需要用到等待呼唤机制
代码修改如下(红色部分 ):
//定义操作的类
class Res{
String name;
String sex;
boolean flag=false;
}
//定义类实现Runnable接口(输入即赋值操作)
class Input implements Runnable{
private Res rs;
Input(Res rs){
this.rs=rs;
}
public void run(){
int x=0;
while(true){
//线程同步解决安全问题
synchronized(rs){
if(rs.flag)
try{ rs.wait();}catch(Exception 3){}
if(x==0){
rs.name="张三";
rs.sex="男";
}else{
rs.name="丽丽";
rs.sex="女";
}
x=(x+1)%2; //相当于布尔类型,是一个死循环
rs.flag=true;
rs.notify();
}
}
}
}
//定义类实现Runnable接口(输出即取值操作)
class Output implements Runnable{
private Res rs;
Output(Res rs){
this.rs=rs;
}
public void run(){
while(true){
//线程同步解决安全问题
synchronized(rs){
if(!rs.flag)
try{ rs.wait();}catch(Exception 3){}
System.out.println(rs.name+"-------------"+sex);
rs.flag=false;
}
}
}
}
//定义测试类
class InputOutputDemo{
public static void main(String []args){
//创建操作资源对象
Res rs=new Res();
//建立线程对象
Input intput=new Input(rs);
Output outtput=new Output(rs);
//将Runnable接口的子类对象作为实际参数传递给Tread类的构造函数
Thread t1=new Thread(intput);
Thread t2=new Thread(output);
t1.start(); //启用线程
t2.start();
}
}
输出的结果:交替打印
张三 男 和 丽丽 女
停止线程:
只有一种,run方法结束。
开启多线程运行,运行代码同常事循环结构。
只要控制住循环,就可以让run线程结束。也就是线程结束。
特殊情况,当线程处于了冻结状态,就不会读到标记,那么线程就不会结束。
当没有指定的方式让冻结的线程恢复到运行状态时,这时需要对冻结进行清除。
强制让线程恢复到运行状态中来,这样就可以操作标记让线程结束。
Thread类提供该方法:interrupt();
守护线程:
也就是后台线程,当所有前台线程结束后,后台线程自动结束。写法:在线程启用前设置调用setDaemon(true);
join方法
例如说当A线程执行到了B线程的join()方法时,A就会等待,等B线程都执行完,A才会执行。join可以用来临时加入线程执行。
优先级&Yield方法
可以设置调用setPriority(int 值1到10),最大级别为10级,最小级别为1级,一般默认是5级。