多线程
一、理解线程和进程
1、进程:
1.一个正在执行的程序
2.进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位
2、线程:
1.就是进程中的一个独立的控制单元。线程在控制着进程的执行。只要进程中有一个线程在执行,进程就不会结束。
2.一个程序至少有一个进程,一个进程至少有一个线程
3、区别:
1.线程的改变只代表了 CPU 执行过程的改变,而没有发生进程所拥有的资源变化。出了 CPU 之外,计算机内的软硬件资源的分配与线程无关。
2.进程拥有一个完整的虚拟地址空间,不依赖于线程而独立存在;反之,线程是进程的一部分,没有自己的地址空间,与进程内的其他线程一起共享分配给该进程的所有资源。
4、多线程:
在java虚拟机启动的时候会有一个java.exe的执行程序,也就是一个进程。
1.该进程中至少有一个线程负责java程序的执行。而且这个线程运行的代码存在于main方法中。该线程称之为主线程。
2.JVM启动除了执行一个主线程,还有负责垃圾回收机制的线程。
3.用户自建的一些线程。
像这种一个进程里面有多个线程执行的方式,就叫做多线程。
看到了3个进程(垃圾回收不考虑了),main线程和Thread-0和Thread-1线程。
5、多线程存在的意义
目的:
创建线程的目的是为了开启一条执行路径,去运行指定代码或者其他代码已实现同步运行。
优点:
1.提高效率:并行执行的效率绝大多情况下要好于串行执行。
2.占用大量处理时间的任务可以定期将处理器时间让给其它任务
3.可以随时停止任务
4.可以分别设置各个任务的优先级以优化性能
弊端:
1.线程是阻塞式的,资源消耗大
2.上下文切换的开销,即从一个线程切换到另外一个线程的开销
3.使用不当,会出现死锁,这是一种逻辑上的错误
4.对共享资源的处理方式会变得麻烦,涉及同步问题
但总体上,多线程的优点是很大的。
6.电脑CPU的多线程运行原理
1.cpu的运行速度很快,如果是单核cpu的话,那么它会采用一种时分复用的方式,以响应速度极快的方式切换线程,使你觉得是多线程操作。
2.多核cpu可以实现真正意义上的多线程,可以使多个核心分别执行一个线程。
cpu的执行是无规律的,哪个线程被cpu执行,或者说抢到了cpu的执行权,哪个线程就执行。而cpu不会只执行一个,当执行一个一会后,又会去执行另一个,或者说另一个抢走了cpu的执行权。至于究竟是怎么样执行的,只能由cpu决定。
二、创建线程的方式
继承Thread类和实现Runnable接口
1.继承方式
1.创建类继承Thread类;
2.覆盖run()方法;
3.创建子类对象;
4.调用start()方法。
重点在于run()方法的覆盖,run()方法中存储的就是线程要执行的代码。
start()方法会以线程的方式调用run()方法,与用户直接调用run()方法不同,通过start()调用run()方法才可以使run()中程序为线程性的。
public class Demo1 {
public static void main(String[] args) {
// TODO Auto-generated method stub
new Test1("小强").start();
new Test2("xiaowang").start();
for(int i=0;i<5;i++){
System.out.println(i+".......线程是:"+Thread.currentThread().getName());
}
}
}
class Test1 extends Thread{
private String name;
public Test1(String name){
this.name = name;
}
public void run(){
for(int i=0;i<5;i++){
for(int j=1;j<9999;j++){}
System.out.println("i="+i+"........线程是"+Thread.currentThread().getName());
}
}
}
class Test2 extends Thread{
private String name;
public Test2(String name){
this.name = name;
}
public void run(){
for(int j=0;j<5;j++){
for(int i=1;i<9999;i++){}
System.out.println("j="+j+"........线程是"+Thread.currentThread().getName());
}
}
}
注意两个方法:
1.Thread类的构造方法,将会自动构建这个线程的名字,形如:Thread-数字。
而他的名字是可以改变的,通过子类的构造函数调用super(String name);会把线程的名字改写为用户自定义的。
2.Thread.currentThread(),获取当前进程对象的方法。
2.接口实现方式
优点:使用接口,避免了创建了一个庞大的线程体系类,只需要在需要使用线程的时候实现Runnable接口就可以了,而且只需要关心run()方法的覆盖就可以了,避免了继承会带来许多不需要的Thread类中的方法。
实现步骤:
1.创建类实现Runnable接口
2.实现Run()方法,将线程要运行的代码存放在该run()方法中
3.主函数中创建Runnable的实例
4.通过Thread(Runnable target)构造方法,将Runnable接口的子类对象作为实参传递给Thread类的构造方法。
5.调用Thread类中start()方法启动线程。start方法会自动调用Runnable接口子类的run方法。
/***
* 卖票功能
* @author LQX
*实现Runnable接口
*/
public class Demo2 {
public static void main(String[] args) {
// TODO Auto-generated method stub
Ticket num = new Ticket();
for(int i = 1;i<4;i++){
Thread t = new Thread(num);
t.start();
}
}
}
class Ticket implements Runnable{
private int num = 500;
@Override
public void run() {
while(true){
if(num>0){
System.out.println("线程..."+Thread.currentThread().getName()+"正在卖票,剩余票....."+num--);
}
}
}
}
3.两种方式的不同
通过覆盖Thread中的run()方法和实现Runnable接口中的run()方法的不同?
查看源码:
public void run() {
if (target != null) {
target.run();
}
}
这是本来的Thread中的run()方法,target是Runnable的子类对象
采用继承Thread重写run()方法,那么该函数被覆盖,然后对象调用start()方法,start()方法调用run();
采用实现Runnable接口有所不同,传递了参数Runnable 子类实例,if判断为真,执行Runnable.run()方法,其实就是通过Thread.run()调用Runnable.run(),后由Thread.start()调用Thread.run()。
4.线程的几种状态
被创建:等待启动,调用start启动。
运行状态:具有执行资格和执行权。
临时状态(阻塞):有执行资格,但是没有执行权。
冻结状态:遇到sleep(time)方法和wait()方法时,失去执行资格和执行权,sleep方法时间到或者调用notify()方法时,获得执行资格,变为临时状态。
消忙状态:stop()方法,或者run方法结束。
注:当已经从创建状态到了运行状态,再次调用start()方法时,就失去意义了,java运行时会提示线程状态异常。
5.线程安全问题
一个线程的run()方法还没有执行完毕,另一个线程也在执行run()方法。那么他们共享的数据将会出现问题。
注:线程安全问题在理想状态下,不容易出现,但一旦出现对软件的影响是非常大的。
比如:
public class Demo2 {
public static void main(String[] args) {
// TODO Auto-generated method stub
Ticket num = new Ticket();
for(int i = 1;i<4;i++){
Thread t = new Thread(num);
t.start();
}
}
}
class Ticket implements Runnable{
private int num =5;
@Override
public void run() {
while(true){
//标注1
if(num>0){
for(int j=0;j<999999;j++){}
System.out.println("线程..."+Thread.currentThread().getName()+"正在卖票,剩余票....."+num--);
}
}
}
}
以上的代码,我在运行时,出现了-1
分析:
主要就是标注1的问题,在run()方法中,执行1条语句不会发生安全性问题,当有两条及以上语句时,可能会出现。
上面的结果中可以看出,num=1时,Thread-0,Thread-1,Thread-2同时进行了num>0判断,都执行了打印,但却由于为共享数据的问题,使得程序没有正确进行。
解决办法:
要是每次操作共享数据时,可以只让一个线程操作,待执行完毕后,返回
数据后,在进行下一次的线程的操作。那么,就需要synchronized(同步)关键字了。
三、synchronized(同步)
java提供关键字synchronized来解决线程不安全的问题。
使用方式:
一种是同步代码块,
二就是同步函数。都是利用关键字synchronized来实现。
1.同步代码块:
synchronized(对象)
{需要被同步的代码}
此处的对象,好比一把锁,线程进去,需要看这个锁是否是true,可以,就进去,并置锁为false,若此期间,有其他线程想要进来,那么,不好意思,锁是false,请等待吧。当一个线程执行完毕后,再把锁置为true,那么下一个线程才可以进去。
/***
* 卖票功能
* @author LQX
*实现Runnable接口,并实现卖票的同步
*/
public class Demo2 {
public static void main(String[] args) {
// TODO Auto-generated method stub
Ticket num = new Ticket();
for(int i = 1;i<4;i++){
Thread t = new Thread(num);
t.start();
}
}
}
class Ticket implements Runnable{
private int num =50;
private Object obj = new Object();
@Override
public void run() {
while(true){
//给程序加同步,即锁
synchronized (obj) {
if(num>0){
try {
//使用线程中的sleep方法,模拟线程出现的安全问题
//因为sleep方法有异常声明,所以这里要对其进行处理
//注意:不可以向上拋出,因为Runnable接口并没有抛异常,子类也不可以抛
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程..."+Thread.currentThread().getName()+"正在卖票,剩余票....."+num--);
}
}
}
}
}
同步后的代码,使用起来非常好用,多个线程对他进行同时操作,他不会出现,同时对共享数据操作,数据的输出或者返回有误的问题了。
2.同步函数
格式:
在函数上加上synchronized修饰符即可。
那么同步函数用的是哪一个锁呢?
函数需要被对象调用。那么函数都有一个所属对象引用。就是this。所以同步函数使用的锁是this。
public class Demo2 {
public static void main(String[] args) {
// TODO Auto-generated method stub
Ticket num = new Ticket();
for(int i = 1;i<4;i++){
Thread t = new Thread(num);
t.start();
}
}
}
class Ticket implements Runnable{
private int num =50;
private Object obj = new Object();
@Override
public void run() {
while(true){
show();
}
}
//直接在函数上用synchronized修饰即可实现同步
public synchronized void show(){
if(num>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程..."+Thread.currentThread().getName()+"正在卖票,剩余票....."+num--);
}
}
}
同步的前提:
a.需要同步的代码至少要有2句,否则,不会出现安全问题。
b.必须要有两个或者两个以上的线程。
c.必须是多个线程使用同一个锁。
如何寻找多线程中的安全问题?
a.明确哪些代码是多线程操作的
b.明确哪些数据是共享的
c.明确多线程运行代码块中哪些语句是操作共享数据的。
单例设计模式-懒加载涉及到的安全问题
先来看一个例子:
/**
* 单例设计模式
* @author LQX
*采用懒加载
*/
public class Single {
private static Single s = null;
private Single(){}
public static Single getInstance(){
if(s==null){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
s = new Single();
}
return s;
}
public static void main(String[] args) {
Demo d1 = new Demo();
Thread t1 = new Thread(d1);
Thread t2 = new Thread(d1);
Thread t3 = new Thread(d1);
Thread t4 = new Thread(d1);
Thread t5 = new Thread(d1);
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
}
}
class Demo implements Runnable{
public void run() {
Single s1 = Single.getInstance();
System.out.println(s1);
}
}
结果,每个线程取到的不是唯一的对象,说明,存在线程安全问题。
解决,加同步代码块或使用同步函数,这里使用同步代码块,因为这里还有一个效率问题。
/**
* 单例设计模式
* 采用懒加载(并使用了安全机制)
*/
public class Single {
private static Single s = null;
private Single() {
}
public static Single getInstance() {
// 这里进行判断,第一次若没有new的话,则进入,以后若已经存在了对象,则不必在进去,效率问题
if (s == null) {
synchronized (Single.class) {
if (s == null) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
s = new Single();
}
}
}
return s;
}
public static void main(String[] args) {
Demo d1 = new Demo();
Thread t1 = new Thread(d1);
Thread t2 = new Thread(d1);
Thread t3 = new Thread(d1);
Thread t4 = new Thread(d1);
Thread t5 = new Thread(d1);
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
}
}
class Demo implements Runnable {
public void run() {
Single s1 = Single.getInstance();
System.out.println(s1);
}
}
可以看到,现在取出来是同一个对象,不存在线程不安全的问题了。
静态同步函数使用:
静态的同步函数使用的锁是该函数所属字节码文件对象,可以用getClass方法获取,也可以用当前类名.class表示。
public class StaticThread {
public static void main(String[] args) {
Ticket1 t = new Ticket1();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.flag = false;
t2.start();
}
}
class Ticket1 implements Runnable{
private static int num = 100;
boolean flag = true;
public void run(){
if(flag){
//需要同步,同步代码块
while(true){
synchronized (this.getClass()) {
if(num>0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+".....runnable...."+num--);
}
}
}
}else{
//这里面已经同步过了
while(true){
show();
}
}
}
public synchronized static void show(){
if(num>0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+".....fuction...."+num--);
}
}
}
死锁示例
/***
* 写一个死锁
* 锁的嵌套就会导致死锁
*/
public class DeadLock {
public static void main(String[] args) {
DeadlockDemo d1 = new DeadlockDemo(true);
DeadlockDemo d2 = new DeadlockDemo(false);
Thread t1 = new Thread(d1);
Thread t2 = new Thread(d2);
t1.start();
t2.start();
}
}
class DeadlockDemo implements Runnable {
public static Object locka = new Object();
public static Object lockb = new Object();
private boolean flag;
public DeadlockDemo(boolean f) {
this.flag = f;
}
public void run() {
if (flag) {
while (true) {
synchronized (locka) {
System.out.println(Thread.currentThread().getName() + "......if locka...");
synchronized (lockb) {
System.out.println(Thread.currentThread().getName() + "......if lockb...");
}
}
}
} else {
while (true) {
synchronized (lockb) {
System.out.println(Thread.currentThread().getName() + "......else lockb...");
synchronized (locka) {
System.out.println(Thread.currentThread().getName() + "......else locka...");
}
}
}
}
}
}
死锁的形成:线程1的运行需要两个锁a和b,而且有顺序,要先获得a锁才可以获得b锁;线程2与1相反,那么,线程1获得a锁,线程2获得b锁,两边都在等待对方先释放锁,那么就会处于无限等待的状态,就是死锁了。
4.线程间的通信
上面讲的同步问题是建立在对同一操作下的不同线程,那么只要加上synchronized关键字就可以解决了。当操作共享资源的是不同的操作,或者说,只是操作同一对象,而操作是完全不同的类时,那么,线程间应该如何保证同步问题。
那就需要用到线程间的通信了:
等待/唤醒机制。
涉及的方法:
1,wait(): 让线程处于冻结状态,被wait的线程会被存储到线程池中。
2,notify():唤醒线程池中一个线程(任意).
3,notifyAll():唤醒线程池中的所有线程。
先写一个简单的例子,生产米饭,做一份米饭,就消费一份米饭。那么只有一个生产者和消费者。
/***
* 多线程的生产者和消费者
*
*/
public class CustomerProducerDemo1 {
public static void main(String[] args) {
Production p = new Production();
Producer pro1 = new Producer(p);
Customer cus1 = new Customer(p);
Thread t1 = new Thread(pro1);
Thread t3 = new Thread(cus1);
t1.start();
t3.start();
}
}
class Production {
private String name;
private int i = 1;
// true表示是没有产品
private boolean flag = true;
public synchronized void setPara(String name) {
if (!flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.name = name + i;
System.out.println("生产者...............生产了:" + this.name);
i++;
flag = false;
this.notify();
}
public synchronized void Out() {
if (flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("消费者.....消费了:" + this.name);
flag = true;
this.notify();
}
}
class Producer implements Runnable {
Production p;
public Producer(Production p) {
this.p = p;
}
public void run() {
while (true) {
p.setPara("米饭");
}
}
}
class Customer implements Runnable {
Production p;
public Customer(Production p) {
this.p = p;
}
public void run() {
while (true) {
p.Out();
}
}
}
运行结果也很简单,生产一份,消费一份,那么,问题来了,如果我是多生产者,多消费者,还是这样吗?
于是,增加生产者,消费者:
Production p = new Production();
Producer pro1 = new Producer(p);
Producer pro2 = new Producer(p);
Customer cus1 = new Customer(p);
Customer cus2 = new Customer(p);
Thread t1 = new Thread(pro1);
Thread t2 = new Thread(pro2);
Thread t3 = new Thread(cus1);
Thread t4 = new Thread(cus2);
t1.start();
t2.start();
t3.start();
t4.start();
结果就不对了:
这就不是先前说好的生产一份,消费一份。
问题分析:
问题就出在wait()和notify()这两个方法的使用上。
public synchronized void setPara(String name) {
if (!flag) {
this.wait();
}
flag = false;
this.notify();
}
public synchronized void Out() {
if (flag) {
this.wait();
}
flag = true;
this.notify();
}
}
我们知道,当只有一个生产者和消费者的话,这段代码的意图在于:
当没有产品时,消费者调用wait()方法,生产者调用生产方法,当生产一件产品后,激活线程池的随机一个线程,置flag为反,生产者调用wait()方法等待。
有产品的时候,消费者消费,并激活线程池的随机一个线程,生产者继续生产,如此往复。
所以,我们看出,问题在于随机二字,也就是说,两个线程,我可以保证调完生产者,调消费者,但是如果有两个及以上的生产者和消费者则不能保证:生产者和消费者是交替调用的。
那么,怎么发生的呢?
1.首先要知道wait()方法使其处于冻结状态,而唤醒他,if()判断不会得到重新判断。
2.生产者和消费者没有交替调用,假设有两个生产者1和2,此时,由于上一次生产,生产者1和2已经处于wait()冻结状态了(此句话的意思也就是if已经判断过了!)。此时,生产者1被消费者唤醒了,它生产一个产品,置flag为反,且随机唤醒一个线程,那么,刚好唤醒了生产者2,而生产者2也是不需要if(flag)的,这就导致了他不会被wait()了,而是直接执行下面的代码了,又继续生产了一个产品,那么此时,就生产了两个产品,且这种机制仍会继续,加入下次,它又唤醒了生产者1,如此,产品会不断生产下去。
解决办法:使用while+notifyALL()这种组合,将if换为while,notify换为notifyAll:
while:解决了唤醒后不判断的缺陷,使得不会出现生产者(消费者)多次生产(消费);
notifyAll:保证至少生产者和消费者可以交替唤醒的。(缺点:开销好大,每次都要唤醒全部,解决:jdk1.5新特性)
思考:
1.如果只是用while(),而不使用notifyAll(),会出现什么情况?
答:死锁,生产者1唤醒了生产者2,生产者2也判断,wait()了,那么,所有线程全部处在冻结状态了。
2.以上情况反过来呢?
答:那问题就没有解决,问题仍会出现。
5.JKK1.5新特性Lock接口
全路径:java.util.concurrent.locks.Lock
实现此接口的类:
ReentrantLock,
ReentrantReadWriteLock.ReadLock,
ReentrantReadWriteLock.WriteLock
使用:
public void m() {
lock.lock(); // block until condition holds
try {
// ... method body
} finally {
lock.unlock()
}
}
这样就保证了锁一定会被解开。
用于替代wait(),notify(),notifyAll()等方法,加入了Condition接口,该接口可以通过lock.newCondition() 方法获取。
实例:将上例生产者-消费者用新特性实现
/***
* 多线程的生产者和消费者jdk1.5的解决方法
*/
public class CustomerProducerDemo1 {
public static void main(String[] args) {
Production p = new Production();
Producer pro1 = new Producer(p);
Producer pro2 = new Producer(p);
Customer cus1 = new Customer(p);
Customer cus2 = new Customer(p);
Thread t1 = new Thread(pro1);
Thread t2 = new Thread(pro2);
Thread t3 = new Thread(cus1);
Thread t4 = new Thread(cus2);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class Production {
private String name;
private int i = 1;
// true表示是没有产品
private boolean flag = true;
// 1.5特性Lock接口
Lock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();// 生产者的监视
Condition condition2 = lock.newCondition();// 消费者的监视
public void setPara(String name) {
// 上锁
lock.lock();
try {
while (!flag) {
try {
condition1.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.name = name + i;
System.out.println(Thread.currentThread().getName() + "生产者...............生产了:" + this.name);
i++;
flag = false;
condition2.signal();
} finally {
lock.unlock();
}
}
public void Out() {
lock.lock();
try {
while (flag) {
try {
condition2.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "消费者.....消费了:" + this.name);
flag = true;
condition1.signal();
} finally {
lock.unlock();
}
}
}
class Producer implements Runnable {
Production p;
public Producer(Production p) {
this.p = p;
}
public void run() {
while (true) {
p.setPara("米饭");
}
}
}
class Customer implements Runnable {
Production p;
public Customer(Production p) {
this.p = p;
}
public void run() {
while (true) {
p.Out();
}
}
}
结果没有出现安全性问题。
优点:
以前一个锁上只能有一组监视器方法。现在,一个Lock锁上可以多组监视器方法对象,可以实现一组负责生产者,一组负责消费者
wait和sleep的区别:
1,wait可以指定时间也可以不指定。
sleep必须指定时间。
2,在同步中时,对cpu的执行权和锁的处理不同。
wait:释放执行权,释放锁。
sleep:释放执行权,不释放锁。
6.停止线程的方式
1,stop方法。
2,run方法结束。
怎么控制线程的任务结束呢?
任务中都会有循环结构,只要控制住循环就可以结束任务。
控制循环通常就用定义标记来完成。
但是如果线程处于了冻结状态,无法读取标记。如何结束呢?
可以使用interrupt()方法将线程从冻结状态强制恢复到运行状态中来,让线程具备cpu的执行资格。
当时强制动作会发生了InterruptedException,记得要处理
接下来,设计一个,不可以通过run()方法循环自动结束的线程,必须使用interrupt()方法。
案例:
/**
* 线程的关闭
*/
public class StopThread {
public static void main(String[] args) {
StopThreadDemo s = new StopThreadDemo();
Thread t1 = new Thread(s);
Thread t2 = new Thread(s);
t1.start();
t2.start();
//如果不使用interrupt方法,那么线程将永远等待下去
t1.interrupt();
t2.interrupt();
}
}
class StopThreadDemo implements Runnable {
private boolean flag = true;
private int num = 150;
public void run() {
while (flag) {
synchronized (this) {
if (num > 0) {
System.out.println(Thread.currentThread().getName() + "........" + num--);
} else {
try {
this.wait();
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "......." + e);
flag = false;
}
}
}
}
}
}
怎么实现的?
interrupt()方法将线程从冻结状态强制恢复到运行状态中来,并抛出InterruptedException,我们只需要在catch区将判断标志改写为结束,那么通过这种方式,将会解决线程无法关闭的问题。
7.其他
线程常见的一些方法。
|--setDaemon()(守护进程,就是后台进程,如果设置为true的话,那么该线程就是后台进程。当正在运行的线程都是守护线程时,Java 虚拟机退出。 )
|--join() (临时加入一个线程运算时可以使用join方法);
|--优先级
|--yield()( 暂停当前正在执行的线程对象,并执行其他线程。);
|--在开发时,可以使用匿名内部类来完成局部的路径开辟。