7、JAVA基础&多线程
进程:正在执行的程序。就是一个应用程序在内存中开辟的空间。
线程:其实就是进程中的一个控制单元。它负责就是程序的执行。
一个进程中至少有一个线程。JVM本身就是多线程的。因为在程序运行过程中会在堆内存产生很多的垃圾。就需要被垃圾回收器进行回收。main函数代码执行时,也在运行着垃圾回收。所以是同时执行的,这就是两个独立线程来进行控制的。执行垃圾回收的线程,称为垃圾回收线程。执行main函数的线程,称为主线程。
创建一个执行路径的目的就是,让单独一个线程去执行指定的代码。和其他代码同时执行。
对于主线程:它的运行的代码都存储在主函数中。
对于垃圾回收线程:它运行就是用于回收对象垃圾的代码。
描述线程的对象是 Thread 类。继承 Thread 类,覆盖其run方法。Thread 本身就是一个类,就是一个线程Thread,直接创建其对象,就是一个线程。run既然是运行方法,那么他里面存储了线程要运行的代码。可是我们想要创建线程运行我们自己制定的代码,那么这时就应该利用继承思想,将 Thread 类进行继承,并覆盖已经有的run方法,定义自己要运行的线程代码。
步骤:
1、继承 Thread 类
2、覆盖 Thread 类中的run方法 在该方法中定义需要运行的代码
3、调用线程对象中的start方法开启线程,并调用线程的run方法
主线程运行的代码在主函数中,自定义线程运行的代码在run方法中。发现程序运行的结果每次都不一样。那是因为多线程的随机性造成的。CPU做着快速的切换,运行着各个线程。随机性原理是由于CPU做着快速的切换造成的。形象的称为多个线程在抢夺cpu的执行权。哪个线程获取到cpu的执行权,哪个线程就执行。
主线程名称为 “main”。其他中定义线程名称 Thread-编号 编号从零开始
创建线程的目的:就为了当前线程和目前正在运行的线程同时执行。
垃圾回收机制:
垃圾回收器在不定时的时候回收堆内存中没有引用的一些对象。有时候垃圾回收器还没有来得急回堆内存中的垃圾,JVM就已经关闭那么此时所有分配的空间全部都被随着JVM的关机而全部释放。每次调用垃圾回收器,打印的结果不一样,原因是垃圾回收线程和其他线程抢夺资源。多线程不容易控制,随机性。
创建线程之后,必须用start开启线程。线程在运行的状态
售票的程序:
class Ticket implements Runnable//extends Thread
{
private int num = 100;
public void run()
{
while(true)
{
if(num>0)
{
System.out.println(Thread.currentThread().getName()+"...sale:"+num--);
}
}
}
}
class TicketDemo
{
public static void main(String[] args)
{
Ticket t = new Ticket();
//用Thread类创建了四个线程。并开启.
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
final finally finalize 区别?
final finally 是两个关键字。final 可以修饰 类 方法 变量 finally 异常中一定能被执行到的语句,通常用于关闭资源。finalize 是一个方法,被垃圾回收器调用垃圾回收器的。
实现 Runnable 接口
现实Runnable接口的步骤:
1、定义类实现 Runnable 接口
2、覆盖Runnable接口中的run方法。将线程要运行的代码存储到run方法中
3、通过Thread类创建线程对象。
4、将实现了Runnable接口的子类对象作为实际参数传递给 Thread 类的构造函数。为什么这么做呢?因为线程对象创建后,必须明确要运行的run方法,而该run方法所属的对象是Runnable接口的子类对象,所以将该子类对象传递Thread类的构造函数
5、调用Thread类的start方法
实现 Runnable 接口的好处:
1、 Runnable 接口的出现,避免了单继承的局限性
2、 Runnable 直接将线程运行的代码(任务)封装Runnable接口类型的对象中,也就是将线程的任务封装成了对象。这样就实现了线程对象和任务对象的解耦。所以这种方法更为常用。
目前所接触的三个线程:主线程、垃圾回收线程、自定义线程(来自于继承 Thread 或者 实现 Runnable)。
线程安全性问题:造成安全问题的原因:
1、多线程同时操作了共享数据
2、多线程的任务代码中操作共享数据的语句不止一条。
class Ticket implements Runnable
{
private int num = 100;
private Object obj = new Object();
public void run()
{
while(true)
{ obj 就代码一个锁而已
synchronized(obj) 同步代码块
{
if(num>0)
{
try{Thread.sleep(10);}catch(InterruptedException e){}
System.out.println(Thread.currentThread().getName()+"...sale:"+num--);
}
}
}
}
}
class TicketDemo2
{
public static void main(String[] args)
{
Ticket t = new Ticket();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
解决思想:让一个线程在执行多条操作共享数据的运算过程中,其他线程不要参与共享数据的操作。
java中的解决线程安全问题方案:加同步。将需要同步的代码封装到了指定同步语句块当中即可。
同步代码块的体现:
synchronized(对象)
{
需要被同步的代码;
}
同步的好处:解决了线程的安全问题。
同步的前提:
1、同步中如果只有一个线程在执行。是没有必要同步的。
2、如果有多个线程需要同步,必须要保证它们使用的一个锁。这个前提的好处:如果在多线程中加入了同步后,还是出现了安全问题的话。这时就可以用这个前提来对程序进行分析。
同步的弊端:对程序性能有一些影响,会降低一些效率。
同步的另一种表现形式:同步函数,就是让函数具备同步性。
同步函数和同步代码块的区别:同步函数使用的锁是: this 。同步代码块使用的锁是任意对象,一般开发建议使用同步代码块。静态同步函数使用的锁是 类名.class 是静态函数所属类的字节码文件对象。
class Bank
{
private int sum;
private Object obj = new Object();
public synchronized void add(int num)//同步函数 这里使用的锁是当前这个对象的引用this
{
sum = sum + num;
try{Thread.sleep(10);}catch(InterruptedException e){}
System.out.println("sum="+sum);
}
}
class Customer implements Runnable
{
private Bank b = new Bank();
public void run()
{
for(int x=0; x<3; x++)
{
b.add(100);
}
}
}
class BankDemo
{
public static void main(String[] args)
{
Customer c = new Customer();
Thread t1 = new Thread(c);
Thread t2 = new Thread(c);
t1.start();
t2.start();
}
}
卖票的实例:要求是一个线程执行同步代码块,一个线程执行同步函数;
class Ticket implements Runnable
{
private static int num = 100;
private Object obj = new Object();
private boolean flag = true;
public void run()
{
if(flag)
while(true)
{
synchronized(obj) 将这里的锁改为this就可以了
{
if(num>0)
{
try{Thread.sleep(10);}catch(InterruptedException e){}
System.out.println(Thread.currentThread().getName()+"...code....:"+num--);
}
}
}
else
while(true)
this.show();
}
public synchronized void show()
{
if(num>0)
{
try{Thread.sleep(10);}catch(InterruptedException e){}
System.out.println(Thread.currentThread().getName()+"......func........:"+num--);
}
}
public void setFlag()
{
flag = false;
}
}
class StaticLockDemo
{
public static void main(String[] args)
{
Ticket t = new Ticket();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
try{Thread.sleep(10);}catch(InterruptedException e){}
t.setFlag();
t2.start();
}
}
通过这个例子发现了同步使用了,但是还是出现了零号票,这个时候什么原因造成的呢?这个时候就需要用前面说过的两个原因来分析,这个时候首先判断是不是同一把锁。经过分析后发现同步代码块执行的时候用的锁是obj锁,而同步函数使用的this这个锁。这个时候可以将同步代码块的锁该为this。
单例设计模式中懒汉式中存在的安全问题
因为懒汉式存在多线程并发访问安全问题,所以需要同步解决现存安全问题,但是同步会降低效率,所以可以通过双重if判断的形式,解决效率问题,降低对锁的判断次数。但是在代码体现上较多,所以开发的时候采用饿汉式,而面试中,常用懒汉式
//饿汉式
class Single
{
private static Single s = new Single();
private Single(){}
public static Single getInstance()
{
return s;
}
}
//懒汉式
class Single
{
private static Single s = null;
private Single(){}
public static Single getInstance()
{
if( s==null )
s = new Single();
return s;
}
}
改良的懒汉式
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;
}
}
}
死锁:面试:容易引发死锁的情况之一:同步的嵌套
为什么是object 中的方法呢?因为这些方法都是必须要标识出来所属的锁。而锁是任意的对象,能被任意对象所访问到的对象,应该是在使用等待唤醒时都需要标记。flag等待唤醒机制中,最常见的体现就是生产者和消费者问题。当多生产多消费者同时出现时,用while做判断,并且用notifyAll来唤醒,此时唤醒中肯定有对方。但是这个只适合单生产者和单消费者,当面对多生产者和多消费者的时候有无法保证了。
等待唤醒机制中,最常见的体现就是生产者消费者问题,发现两个问题:
1,出现了错误的数据。是因为多生产和多消费的时候,被唤醒的线程没有再次判断标记就执行了。解决是将if判断变成while判断。
2,发现有了while判断后,死锁了。因为本方线程唤醒的有可能还是本方线程。所以导致了死锁。解决:本方必须唤醒对方才有效。notify只能唤醒一个,还不确定。所以干脆唤醒全部,肯定包含对方,至于被唤醒的本方,会判断标记是否继续等待。
死锁实例:面试题 class Test implements Runnable { private boolean flag = true; Test(boolean flag) { this.flag = flag; } public void run() { while(true) { if(flag) { synchronized(MyLock.lock_a) { System.out.println(Thread.currentThread().getName()+"..if lock_a"); synchronized(MyLock.lock_b) { System.out.println(Thread.currentThread().getName()+"..if lock_b"); } } } else { synchronized(MyLock.lock_b) { System.out.println(Thread.currentThread().getName()+"..else lock_b"); synchronized(MyLock.lock_a) { System.out.println(Thread.currentThread().getName()+"..else lock_a"); } } } } } } class MyLock { public static Object lock_a = new Object(); public static Object lock_b = new Object(); } class DeadLockTest { public static void main(String[] args) { Test t1 = new Test(true); Test t2 = new Test(false); new Thread(t1).start(); new Thread(t2).start(); } } |
/* 演示死锁。 容易引发死锁的情况之一:同步的嵌套。 */ class Ticket implements Runnable { private int num = 100; private Object obj = new Object(); private boolean flag = true; public void run() { if(flag) while(true) { synchronized(obj) { show(); } } else while(true) this.show(); } public synchronized void show() 这个锁是this { synchronized(obj) { if(num>0) { try{Thread.sleep(10);}catch(InterruptedException e){} System.out.println(Thread.currentThread().getName()+"......sale........:"+num--); } } } public void setFlag() { flag = false; } } class DeadLockDemo { public static void main(String[] args) { Ticket t = new Ticket(); Thread t1 = new Thread(t); Thread t2 = new Thread(t); t1.start(); try{Thread.sleep(10);}catch(InterruptedException e){} t.setFlag(); t2.start(); } } |
线程间通信:多个线程在处理同一个资源,但处理的动作却不一样,动作不同意味着线程的任务是不一样的,就需要对任务进行单的描述和封装。
/* 线程间通信: 多个线程在处理同一个资源,但是处理的动作却不一样。 动作不同意味着线程的任务是不一样的,就需要对任务对象进行单独的描述和封装。 */ class Resource // 创建资源 { String name; /* 线程间通信: 多个线程在处理同一个资源,但是处理的动作却不一样。 动作不同意味着线程的任务是不一样的,就需要对任务对象进行单独的描述和封装。 */ class Resource // 创建资源 { String name; String sex; } class Input implements Runnable //定义线程任务 输入数据 { Resource r; Input(Resource r) { this.r = r; } public void run() { int x = 0; while (true) { synchronized(r) // 同一把锁就是这个资源对象 { if(x==0) { r.name = "mike"; r.sex = "nan"; } else { r.name = "丽丽"; r.sex = "女女女女女"; } } x = (x+1)%2; } } } class Output implements Runnable //定义线程任务 取出数据 { Resource r; Output(Resource r) { this.r = r; } public void run() { while(true) { synchronized(r) // 同一把锁就是这个资源对象 { System.out.println(r.name+"....."+r.sex); } } } } class ResourceDemo { public static void main(String[] args) { Resource r = new Resource(); // 创建资源 Input in = new Input(r); // 建立输入线程对象 Output out = new Output(r); // 建立输出线程对象 Thread t1 = new Thread(in); Thread t2 = new Thread(out); t1.start(); t2.start(); } } |
线程间通信最重要的机制,等待唤醒机制.
wait() 等待 让当前线程处于冻结状态,当前线程就被存储到了线程池中。
notify(); 唤醒 唤醒线程池中的任意一个线程,让该线程恢复到运行状态,会具备CPU的执行资格
notifyall(); 唤醒全部 唤醒线程池中的所有等待线程。具备CPU的执行资格
wait() notify() 必须使用在同步当中。要标识操作的线程中所属的同一步锁。
举例就是:抓人的游戏。
notify必须要用到同步中,要标识操作的线程所属的同步的锁。换句话说:wait,到底是让哪个锁上的线程等待了。notify,到底是唤醒了哪个锁上被等待的线程。
而这些方法在Object类中。为什么是Object中的方法呢?因为这些方法都是必须要标识出所属的锁,而锁是任意的对象。能被任意对象调用的方法一定定义在Object类中。
在这个例子中,输入数据和输出数据都只有一个线程。即就是单输入,单输出。此题如果在多输入和多输出就又会出问题 class Resource { String name; String sex; boolean flag = false; } class Input implements Runnable { Resource r; Input(Resource r) { this.r = r; } public void run() { int x = 0; while (true) { synchronized(r) // 当一个输入线程获取锁后,开始执行 { if(r.flag) // 判断标记, try{r.wait();}catch(Exception e){} if(x==0) { r.name = "mike"; r.sex = "nan"; } else { r.name = "丽丽"; r.sex = "女女女女女"; } r.flag = true; // 当这个输入线程执行结束后,将标记该为真 r.notify(); // 并且唤醒其他线程,但这里还有问题, //因为notify 只能唤醒一个线程,这时我们应该标明唤醒那个线程中的锁 } x = (x+1)%2; } } } class Output implements Runnable { Resource r; Output(Resource r) { this.r = r; } public void run() { while(true) { synchronized(r) // 当一个输出线程获取锁后,开始执行 { if(!r.flag) // 判断标记, try{r.wait();}catch(Exception e){} System.out.println(r.name+"....."+r.sex); r.flag = false; // 当这个输出线程执行结束后,将标记该为真 r.notify(); // 并且唤醒其他线程,但这里还有问题 } //因为notify 只能唤醒一个线程,这时我们应该标明唤醒那个线程中的锁 } } } class ResourceDemo2 { public static void main(String[] args) { Resource r = new Resource(); Input in = new Input(r); Output out = new Output(r); Thread t1 = new Thread(in); Thread t2 = new Thread(out); t1.start(); t2.start(); } } 上面代码需要要优化。因为输入数据和输出数据都应该是资源的自身的属性。这个时候可以用get和set对外提供方法。 class Resource { 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 { Resource r; Input(Resource r) { this.r = r; } public void run() { int x = 0; while (true) { if(x==0) r.set("mike","nan"); else r.set("丽丽","女女女女女"); x = (x+1)%2; } } } class Output implements Runnable { Resource r; Output(Resource r) { this.r = r; } public void run() { while(true) { r.out(); } } } |
等待唤醒机制中,最常见的体现就是生产者消费者问题
发现两个问题:
1,出现了错误的数据。是因为多生产和多消费的时候,被唤醒的线程没有再次判断标记就执行了。解决是讲if判断变成while判断。
2,发现有了while判断后,死锁了。因为本方线程唤醒的有可能还是本方线程。所以导致了死锁。
解决:本方必须唤醒对方才有效。notify只能唤醒一个,还不确定。所以干脆唤醒全部,肯定包含对方,至于被唤醒的本方,会判断标记是否继续等待。
class Resource { private String name; private int count; private boolean flag; public synchronized void set(String name) { while(flag) try{this.wait();}catch(Exception e){}// (活) t1 (活)t0 this.name = name+count; count++; System.out.println(Thread.currentThread().getName()+"....生产者..."+this.name);//生产者 商品0 生产者 商品1 商品2 flag = true; notifyAll(); //唤醒所有 } public synchronized void out()// { while(!flag) try{this.wait();}catch(Exception e){}//t2 t3 System.out.println(Thread.currentThread().getName()+"......消费者......."+this.name);//消费 商品0 flag = false; notifyAll(); //唤醒所有 } } class Producer implements Runnable { private Resource r; Producer(Resource r) { this.r = r; } public void run() { while(true) { r.set("商品"); } } } class Consumer implements Runnable { private Resource r; Consumer(Resource r) { this.r = r; } public void run() { while(true) { r.out(); } } } class ProConDemo { public static void main(String[] args) { Resource r = new Resource(); Producer pro = new Producer(r); Consumer con = new Consumer(r); Thread t0 = new Thread(pro); Thread t1 = new Thread(pro); Thread t2 = new Thread(con); Thread t3 = new Thread(con); t0.start(); t1.start(); t2.start(); t3.start(); } } |
JDK1.5版本后,对多线程中的内部细节进行了升级改良。在java.util.concurrent.locks包中提供了一个Lock接口。
Lock接口中提供了 lock()获取锁 unlock释放锁的操作。Lock接口更符合面向对象的思想,将锁这种事物封装成了对象。
public void run()
{
synchronized(obj)
{//获取锁。
code...
//释放锁。
}
}
但是对于释放和获取锁的操作,都是隐式的。JDK1.5后,就有了新的方法,将锁封装成了对象。因为释放锁和获取锁动作,锁自己最清楚。 锁对象的类型就是Lock接口。并提供了,显示的对锁的获取和释放的操作方法。
Lock lock;
public void run()
{
try
{
lock.lock();//获取锁。
code...throw ...
}
finally
{
lock.unlock();//释放锁.
}
}
Lock接口替代了synchronized 。Condition替代了Object类中监视器方法 wait notify notifyAll。将监视器方法单独封装成了Condition对象。而且一个锁上可以组合多组监视器对象。实现了多生产者多消费者时,本方只唤醒对方中一个的操作,提高效率。
import java.util.concurrent.locks.*;
class Resource
{
private String name;
private int count;
private boolean flag;
private Lock lock = new ReentrantLock();
private Condition con1 = lock.newCondition();//一组监视器监视生产者
private Condition con2 = lock.newCondition();//一组监视器监视生产者
public void set(String name)//
{
lock.lock();//获取锁 .
try
{
while(flag)
try{con1.await();}catch(Exception e){} // (活) t1 (活)t0
this.name = name+count;
count++;
System.out.println(Thread.currentThread().getName()+"....生产者..."+this.name);//生产者 商品0 生产者 商品1 商品2
flag = true;
con2.signal();
}
finally
{
lock.unlock();//释放锁。
}
}
public void out()//
{
lock.lock();
try
{
while(!flag)
try{con2.await();}catch(Exception e){}//t2 t3
System.out.println(Thread.currentThread().getName()+"......消费者......."+this.name);//消费 商品0
flag = false;
con1.signal();
}
finally
{
lock.unlock();
}
}
}
class Producer implements Runnable
{
private Resource r;
Producer(Resource r)
{
this.r = r;
}
public void run()
{
while(true)
{
r.set("商品");
}
}
}
class Consumer implements Runnable
{
private Resource r;
Consumer(Resource r)
{
this.r = r;
}
public void run()
{
while(true)
{
r.out();
}
}
}
class ProConDemo2
{
public static void main(String[] args)
{
Resource r = new Resource();
Producer pro = new Producer(r);
Consumer con = new Consumer(r);
Thread t0 = new Thread(pro);
Thread t1 = new Thread(pro);
Thread t2 = new Thread(con);
Thread t3 = new Thread(con);
t0.start();
t1.start();
t2.start();
t3.start();
}
}
多线程中的等待和唤醒机制:
wait()和sleep()区别 面试题
1、sleep 必须指定时间,wait可以指定时间,也可以不指定时间
sleep 和 wait 都可以让线程临时冻结,释放执行权,cpu不在处理当前线程
2、wait方法必须定义在同步中,sleep不一定
3、在同步中的wait和sleep的对于执行权和锁的处理不同。
3.1 sleep释放CPU的执行权,但在同步中线程不释放锁
3.2 wait释放CPU的执行权,同时线程也释放锁。
停止线程:
stop方法已经过时。
所以只剩了一种办法,线程执行的代码结束,线程会自动终止。
1,run方法中通常都有循环语句,所以只要让循环结束即可,所以只要控制住循环的条件。最简单的方式就是定义标记。
2、如果run方法中有同步可以让线程处于冻结状态方法,比如wait,那么线程就不会取读取标记。那么线程的循环也就无法结束,run方法也不会结束。这时必须让线程恢复到运行状态才可以有机会读取到标记。所以可以通过正常的恢复方法比如notify , sleep 时间到,但是如果没有办法正常恢复,就必须使用强制手段,interrupt方法,强制将线程的冻结状态清除,让其恢复到运行状态,因为只要线程运行就可以读取标记,结束。强制动作会发生异常,需要处理。
前后台线程:main线程 通过start 开启的线程都是属于前台线程。当一个线程被setDeamon标记后,就变成了后台线程。前后台线程没有太大区别,他们都在抢夺cpu的执行权,但当前台线程结束后,后台线程也会随着JVM的退出而结束。
Thread 类中的 toString yield setPriority方法 线程的优先级的数据范围1到10。
setPriority 设置线程的优先级
join() 方法使用的时候会抛出异常:等待该线程终止。一般用于在执行当中临时加入这个线程。
class ThreadDemo2
{
public static void main(String[] args)
{
new Thread()
{
public void run()
{
for(int x=0; x<90; x++)
{
System.out.println(Thread.currentThread().getName()+"....x======"+x);
}
}
}.start();
Runnable r = new Runnable()
{
public void run()
{
for(int y=0; y<90; y++)
{
System.out.println(Thread.currentThread().getName()+"....y======"+y);
}
}
};
Thread t = new Thread(r);
t.start();
for(int z=0; z<90; z++)
{
System.out.println(Thread.currentThread().getName()+"....z======"+z);
}
}