java基础--多线程

时间:2022-05-19 11:23:08

      java多线程的应用非常广泛,主要是为了发挥服务器多处理器的效率。在我们的web编程中应用非常广泛。允许多用户并发同时访问,同时下载多个图片等等均是应用了多线程。但在编程的时候好像关于多线程的代码感觉不到是因为我们将多线程继承到框架里面了,Servelet就是一个单实例多线程的应用。


一、多线程实现


     多线程的创建主要有三种方式:继承Thread接口,继承Runable接口,使用callable和future创建线程。


     (1)继承Thread类实现多线程的步骤如下:

//(1)继承Thread接口
public class FirstThread extends Thread{
private int i;
//(2)重写Run()方法
public void run(){
for(;i<100;i++){
System.out.println(getName()+" "+i);
}
}
}

public static void main(String[] args){
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+" "+i);
if(i==20){
//(3)使用start()方法启动多线程
new FirstThread().start();
new FirstThread().start();
}
}
}

     该实例实现了3个线程,main()方法为主线程,通过两个start()方法创建子线程。该方法实现的运行结果都是从1开始输出,因此通过Thread接口创建线程类无法共享实例变量。


    (2)通过Runable接口创建线程类

//(1)继承runable接口
public class SecondThread implements Runable{
private int i;
//(2)重写run()方法
public void run(){
for(;i<100;i++){
System.out.println(Thread.CurrentTread().getName()+" "+i);
}
}
//多个线程可以共享同一个线程类的实例变量
public static void main(String[] args){
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+" "+i);
if(i==20){
SecondThread st=new SecondThread();
//(3)通过start()方法创建线程
new Thread(st,"新线程1").start();
new Thread(st,"新线程2").start();
}
}

}
}

      运行上面的结果,两个线程输出都是逐渐增加的数字,表明多线程可以共享县城类的实例变量。


     (3)使用Callable和Future创建线程

public class ThirdThread{
public static void main(String[] args){
ThirdThread rt=new ThirdThread();

//(1)使用Callable类定义内部线程类
//(2)使用Futuretask对线程类进行包装
FutureTask<Integer> task=new FutureTask<Integer>((Callabel<Integer>)()->{
int i=0;
for(;i<100;i++){
System.out.println(Thread.currentThread().getName()+" "+i);
}
return i; //call()方法可以有返回值
});

for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getname());
if(i==20){
//(3)使用start方法定义线程对象
new Thread(task,"有返回值的线程").start();
}
}
try{
System.out.println("子线程的返回值"+task.get()); //使用线程的get()方法获得返回值
}catch(Exception ex){
ex.printStackTace();
}
}
}

     使用该方式可以使线程有返回值。


二、线程生命周期


        线程的声明周期要经历创建,就绪,运行,阻塞,死亡五中状态。

 

     使用new来创建线程的那一刻起线程就处于创建状态,运行start()方法则处于就绪状态,但线程处于就绪状态不一定马上开始运行,它要通过JVM的线程调度机制来分配资源(栈和计数器),则线程才能处于运行,注意多线程操作在任何一个时刻也只是由一个线程处于运行状态。


     并不是所有的线程都会一直运行完毕,它需要中断,让出系统资源给其他线程,这个中断的状态就是阻塞状态.当线程类方法执行完毕则处于死亡状态,处于死亡状态的类不能通过start()重新调用。


三、常用线程控制方法


    (1)join()---控制一个线程必须等待另一个线程执行完成才能执行

public class JoinThread extends Thread{
public JoinThread(String name){
super(name);
}
public void run(){
for(int i=0;i<100;i++){
System.out.println(getName()+" "+i);
}
}

public static void main(String[] args){
New JoinThread("新线程").start();
for(int i=0;i<100;i++){
if(i==20){
JoinThread jt=new JoinThread("被Join的线程");
jt.start();
//Main线程必须等jt执行结束后才会向下执行
jt.join();
//主线程处于阻塞状态,直到jt运行完成,main()才能接着执行
}
Syste.out.println(Thread.currentThread().getName()+" "+i);
}
}

}

     (2)后台线程Daemon Thread---为前台提供服务,前台线程执行完毕,后台线程自动停止

public class DaemonThread extends Thread{
public void run(){
for(int i=0;i<1000;i++){
System.out.println(getName()+" "+i);
}
}

public static void main(String[] args){
DaemonThread t=new DaemonThread();

//将此线程设置成后台线程
t.setDaemon(true);

//启动后台进程
t.start();

//for循环执行完毕,t的方法自动结束
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+" "+i);
}

}
}

   (3)sleep()---线程睡眠,暂停处于阻塞状态

public class SleepTest{
public static void main(string[] args){
for(int i=0;i<10;i++){
System.out.println("当前时间:"+new Date());

//调用sleep()方法让当前线程暂停1s
Thread.sleep(1000);
}
}
}

    (4)yield()---线程暂停处于就绪状态

public class YieldTest extends Thread{
public YieldTest(String name){
super(name);
}

public void run(){
for(int i=0;i<50;i++){
System.out.println(getName()+" "+i);
if(i==20)
{
Thread.yield();
}
}
}


public static void main(String[] args)throws Exception{

//启动两个并发进程
YieldTest yt1=new Yieldtest("高级");

//将yt1线程设置成最高优先级
yt1.setPriority(Thread.MAX_PRIORITY);

yt1.start();

//将yt2线程设置成最低优先级
YieldTest yt2=new YieldTest("低级");
yt2.setPriority(Thread.MIN_PRIORITY);

yt2.start();
}
}

    当线程调用yield()方法后,如果存在优先级高于或等于本身的线程则执行其他线程,否则继续执行本身线程。


(四)线程安全问题

      

     线程安全问题是多线程必须要考虑的问题,线程安全是由于变量的非原子性所导致的,有网友归结线程不安全的原因要同时满足下面3个:

     1.多线程环境;

     2.存在共享变量; 

     3.多个线程同时操作共享变量。

     

     最经典的实例是,对于同一张银行卡,基数是500元,如果两个人可以同时对它进行操作,甲存钱,乙取钱,两个人看到的都是500,甲存100,认为卡里的钱应该是600,乙取100,认为卡里的钱应该是400,这样导致数据脏读。解决的方式是加锁。


      解决方式一:同步代码块

     

      通过同步监视器进行监视,sychronized(obj),作用就是阻止两个线程对同一个共享资源进行并发访问。例如对取钱操作的监视器代码实例:

//同步代码块
public void run(){

//使用acount作为同步监视器,任何线程进入下面同步代码块之前
//必须先将acount账户锁定--其他线程无法得到锁,也就无法修改
//这种做法符合:“加锁--修改--修改锁”
synchronized(acount){
if(acount.getBalance()>=drawAmount){
System.out.println(getName()+"取钱成功!吐出钞票"+drawAmount);
try{
Thread.sleep(1);
}catch(InterruptedException ex){
ex.printStackTace();
}

//修改余额
acount.setBalance(acount.getBalance()-drawAmount);
System.out.print("\t余额为:"+acount.getBalance());
}else{
System.out.print(getname()+"取钱失败!余额不足!");
}
}
}

      解决方式二:线程同步方法


      线程同步方法使用sychronized对方法进行修饰。


//线程安全的draw()方法
public sychronized void draw(double drawAmount){
//只对那些会改变竞争资源的方法进行同步;
//如果可变类和有单线程和多线程两种版本,则提供线程安全和线程不安全版本
if(balance>=drawAmount){
...;
}
}


     解决方式三:同步锁--lock


     对比于线程同步方法和代码块同步,lock更加灵活,实现线程安全的控制,常用的是ReentrantLock(可重入锁)。

class x{
//定义锁对象
private final ReentrantLock lock=new ReentrantLock();

public void m(){
lock.lock();
try{
//线程安全的代码
}
finally{
lock.unlock();
}
}
}

      死锁


   跟数据库锁的概念类似,两个线程等待对方释放同步监视器时就会发生死锁,多个线程处于阻塞状态,无法继续,多个同步监视器的情况下很容易发生死锁。例如下面的程序:

class A{
public synchronize void foo(B b){
System.out.println("当前线程名:"+Thread.currentThread().getName()+"进入A实例的foo()方法");
try{
Thread.sleep(200);
}catch(InterruptedException ex){
ex.printStackTrance();
}
//调用B的last方法
b.last();
}
public synchronized void last(){
System.out.println("进入a类的last()方法内部");
}
}

class B{
public synchronized void bar(A a){
System.out.println("当前线程名称:"+Thread.currentThread().getName()+"进入了B实例的bar()方法");
try{
Thread.sleep(200);
}catch(InterruptedExcption ex){
ex.printStackTrace();
}
a.last();
}
public synchronized void last(){
System.out.println("进入B类的last()方法");
}
}
public class DeadLock implements Runnable{
A a=new A();
B b=new B();
//线程A的同步方法包含b的方法
//线程B的同步方法包含a的方法
public void init(){
Thread.currentThread().setName("主线程");
a.foo(b);
System.out.println("进入了主线程之后");
}
public void run(){
Thread.currentThread().setName("副线程");
b.bar(a);
System.out.pirntln("进入副线程之后");
}

public static void main(String[] args){
DeadLock d1=new DeadLock();
new Thread(d1).start();
d1.init();
}
}

     上面程序运行,副线程b运行,睡眠200ms,同时,主线程a运行,副线程b执行a.last()方法时需要先对a进行加锁,但是此时a处于加锁状态;a执行b.last()方法需要对b进行加锁,但此时b也处于加锁状态,两者都在等待对方释放锁,但都不会释放,造成死锁状态。


    线程不安全类的处理--包装


    我们所熟知的Arraylist、LinkedList、HashSet、Treeset、HashMap、TreeMap等都是线程不安全的,高并发线程向这些集合中存取元素时,可能破坏集合数据完整性。


    collection提供了大量的静态方法,可以将不安全类包装成为安全类。eg:

    HashMap m=Collections.synchronizedMap(new HashMap());

 

    此外,在java.util.concurrent包下面提供了大量的支持高效并发访问的集合接口和实现类。主要包含:

    

   (1)以Concurrent开头的集合类,如ConcurrentHashMap

   (2)以CopyOnWrite开头的集合类,如CopyOnWriteArrayList


 六、总结


     了解了线程的产生,作用以及注意事项,对我们在今后开发应用中起着非常重要的作用。