多线程是Java中一个非常重要的概念,它能使得一个进程中多个模块同时运行。比如常见的杀毒软件,在扫描木马的时候还能同时扫面插件、清理垃圾等等。如果一次只能做一件事情,那效率就低下许多了,但是多线程中可能存在安全隐患,应用的时候必须要注意。
进程:是一个正在执行中的程序。每一个进程执行都有一个执行顺序,该顺序是一个执行路径,或者叫一个控制单元。
线程:就是进程中的一个独立的控制单元。线程在控制着进程的执行,一个进程中至少有一个线程。
JVM启动的时候会有一个进程java.exe。该进程中至少一个线程负责java程序的执行。而且这个线程运行的代码存在于main方法中。该线程称之为主线程。
扩展:其实更细节说明jvm,jvm启动不止一个线程,还有负责垃圾回收机制的线程。
自定义一个线程有两种方式:
1. 定义一个类继承Thread,并复写其中的run()方法,并且new这个类后调用start方法启动。
2. 定义一个类实现Runnable接口,并复写其中的run()方法,再用new Thread(new 这个类).start方法启动。
下面用一个例子来说明这两种方法:
package test;
public class Test4 {
//用两种方式启动两个线程,加上主线程,共3个线程,各自打印名字+i
public static void main(String[] args) {
new Demo1().start();
new Thread(new Demo2()).start();
for(int i=0;i<30;i++){
System.out.println(Thread.currentThread().getName()+"_"+i);
}
}
}
//继承Thread类的实现方式
class Demo1 extends Thread{
public void run(){
for(int i=0;i<30;i++){
System.out.println(Thread.currentThread().getName()+"_"+i);
}
}
}
//实现Runnable接口的实现方式
class Demo2 implements Runnable{
public void run(){
for(int i=0;i<30;i++){
System.out.println(Thread.currentThread().getName()+"_"+i);
}
}
}
随便截取一小段打印结果:
Thread-0_6
Thread-0_7
main_0
Thread-0_8
main_1
Thread-1_0
Thread-0_9
Thread-1_1
main_2
Thread-1_2
Thread-0_10
Thread-1_3
main_3
Thread-1_4
可以看出3个线程是并发执行的,交替打印,并无先后顺序。这便是多线程的好处所在了,可以同时执行多个任务,对于程序的效率性大规模提高。
但是多线程在访问同一个变量的时候,会出现安全性的问题,因为程序执行时一个CUP只能同时执行一个线程,多个线程是随机交替执行,可能导致一个线程还没运行完,另一个线程操作了这个数据,可能出现错误。
导致安全问题的出现的原因:
1. 多个线程访问出现延迟。
2. 线程随机性。
注:线程安全问题在理想状态下,不容易出现,但一旦出现对软件的影响是非常大的。
所以,为了解决这个问题,便出现了线程同步的操作。
线程的同步:
格式:synchronized(锁的对象) { 需要同步的代码;}
特点:
同步的前提:
1. 同步需要两个或者两个以上的线程
2. 多个线程使用的是同一个锁
未满足这两个条件,不能称其为同步。
同步的弊端:
当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。
下面演示同步的安全问题:
package test;
public class Test5 {
public static void main(String[] args) {
Cus c = new Cus();
Thread t1 = new Thread(c); //创建2个客户存钱,检测程序的安全问题
Thread t2 = new Thread(c);
t1.start();
t2.start();
}
}
class Bank
{
private int sum;
public void add(int n)
{
// synchronized(this) //注释掉的部分是同步代码,打印出有同步代码,和无同步代码的结果
// {
sum = sum + n;
try{Thread.sleep(10);}catch(Exception e){} //为了看到现象,每次线程等待0.01秒
System.out.println("银行总存款 sum="+sum);
// }
}
}
//这个一个客户类,每个客户每次存入银行100元,共存3次
class Cus implements Runnable
{
private Bank b = new Bank();
public void run()
{
for(int x=0; x<3; x++)
{
b.add(100);
}
}
}
不加同步代码,打印结果为:银行总存款 sum=200
银行总存款 sum=200
银行总存款 sum=400
银行总存款 sum=400
银行总存款 sum=600
银行总存款 sum=600
可以看到,这里存在很严重的安全问题,可能因为多线程的随机执行问题,使得打印结果错误,即程序出现了安全问题,必须用同步解决。
去掉同步的注释,即加上同步代码,打印结果为:
银行总存款 sum=100
银行总存款 sum=200
银行总存款 sum=300
银行总存款 sum=400
银行总存款 sum=500
银行总存款 sum=600
这个是理想的结果一样,成功解决了安全问题。所以在使用多线程时必须注意安全性的问题,这个隐患发生的概率是很小,但是一旦发生,就是致命的问题。
同步中的synchronized可以直接加在方法上,当作修饰符用,这样就不能指定同步的锁了。那么此时用到的锁是哪个呢?在非静态的方法中,同步用到的锁就是对象本身this。而在静态方法中,同步用到的是该方法所在类的字节码文件对象,即 (类名.class) 文件。
线程间的通信:
多个线程间是计算机随机交易执行的,那么有没有方法使得它们按照一定顺序执行呢?如果有,那么各个线程之间必然有一个通信的方式,使得线程知道对方的状态,这便是线程间的通信了。
首先,我们需要知道线程到底存在哪几个状态:
package test;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Test6 {
public static void main(String[] args) throws InterruptedException {
Resource res = new Resource(); //创建一个源类,生成、消费公用里面的数据
Produce pro = new Produce(res);
Consume con = new Consume(res);
new Thread(pro).start(); //两个生产线程、两个消费线程
new Thread(pro).start();
new Thread(con).start();
new Thread(con).start();
}
}
//建立一个商品生产、消费的类,要求交替执行线程,生产一个商品,消费一个商品。
class Resource{
private String name; //商品名字
private int count = 1; //商品编号
private boolean flag = false;
//创建用于同步线程的锁,和新特性Condition
private Lock lock = new ReentrantLock();
private Condition condition_pro = lock.newCondition();
private Condition condition_con = lock.newCondition();
//定义一个生产商品的方法
public void produce(String name) throws InterruptedException{
lock.lock();
try {
while (flag) //标记为了按生产一个消费一个的顺序执行
condition_pro.await();
this.name = name + (count++);
System.out.println(Thread.currentThread().getName() + "。。生产了。。"+ this.name);
flag = true;
condition_con.signal();
}
finally{
lock.unlock(); //释放锁必须执行
}
}
//定义一个消费商品的方法
public void consume() throws InterruptedException{
lock.lock();
try{
while(!flag)
condition_con.await();
System.out.println(Thread.currentThread().getName()+"。。消费了。。。。"+this.name);
flag = false;
condition_pro.signal();
}
finally{
lock.unlock();
}
}
}
//定义生产商品的线程
class Produce implements Runnable{
private Resource res;
Produce(Resource res){
this.res = res;
}
public void run(){
while(true){
try {
res.produce("商品");
} catch (Exception e) {
System.out.println("线程出问题了");
}
}
}
}
//定义消费商品的线程
class Consume implements Runnable{
private Resource res;
Consume(Resource res){
this.res = res;
}
public void run(){
while(true){
try {
res.consume();
} catch (Exception e) {
System.out.println("线程出问题了");
}
}
}
}
这个程序运行结果截取一部分:
Thread-0。。生产了。。商品14639
Thread-2。。消费了。。。。商品14639
Thread-1。。生产了。。商品14640
Thread-3。。消费了。。。。商品14640
Thread-0。。生产了。。商品14641
Thread-2。。消费了。。。。商品14641
Thread-1。。生产了。。商品14642
Thread-3。。消费了。。。。商品14642
Thread-0。。生产了。。商品14643
Thread-2。。消费了。。。。商品14643
Thread-1。。生产了。。商品14644
Thread-3。。消费了。。。。商品14644
Thread-0。。生产了。。商品14645
Thread-2。。消费了。。。。商品14645
运行结果整齐的交替打印,并无安全问题发生,可见用新特性来实现同步有着效率更高的好处。