Java 线程和锁:并发编程的关键概念

Java 线程和锁:并发编程的关键概念

Java 线程和锁:并发编程的关键概念: 现代操作系统在运行一个程序时, 会为其创建一个进程。 

例如, 启动一个Java程序, 操作系统就会创建一个Java进程。

现代操作系统调度的最小单元是线程, 也叫轻量级进程(Light Weight Process) , 在一个进程里可以创建多个线程, 这些线程都拥有各自的计数器、 堆栈和局部变量等属性, 并且能够访问共享的内存变量。 处理器在这些线程上超高速切换, 从而让使用者感觉到这些线程在同时执行。

不得不提下进程和线程的区别,已经并行和并发的区别:

进程 是有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个线程。比如在windows系统中,一个运行的abc.exe就是一个进程。
线程 是指进程中的一个执行任务(控制单元),堆空间是共享的,栈空间是独立的,线程消耗的资源也比进程小,相互之间可以影响的,又称为轻型进程或进程元。一个进程可以同时并发运行多个线程,如:多线程下载软件多任务系统,该系统可以运行多个进程,一个进程也可以执行多个任务,一个进程可以包含多个线程.
一个进程至少有一个线程,为了提高效率,可以在一个进程中开启多个执行任务,即多线程
因为一个进程中的多个线程是并发运行的,那么从微观角度上考虑也是有先后顺序的,那么哪个线程执行完全取决于CPU调度器(JVM来调度,程序员是控制不了的)。
我们可以把多线程并发性看作是多个线程在瞬间抢CPU资源,谁抢到资源谁就运行,这也造就了多线程的随机性。
Java程序的进程里至少包含主线程和垃圾回收线程(后台线程)。
并行:指两个或多个事件在同一时刻点发生。
并发:指两个或多个事件在同一时间段内发生。

二、常用的创建线程的四种方式:

创建线程方式1: 继承Thread类:

public class Way1 extends Thread {
    @Override
    public void run() {
        for( int i=0; i<50; i++ ){
            System.out.println("Way1" + i);
        }
    }
}

创建线程方式2:实现Runnable 接口

public class Way2 implements Runnable {
    @Override
    public void run() {
        for( int i=0; i<50; i++ ){
            System.out.println("Way2" + i);
        }
    }
}

创建线程方式3:实现 Callable接口

public static void main(String[] args) {                    
        //FutureTask可以拿到某个线程的运行结果
        FutureTask<String> f = new FutureTask<>( new swiming() );
        //添加进线程
        Thread tf = new Thread(f);
        tf.start();
        try {
            String val = f.get(); //看运行结果
            System.out.println( val );
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }   
    }
}
//Callable 可以返回结果 
class swiming implements Callable<String>{
    @Override
    public String call() throws Exception {
        for(int i=0; i<50; i++){
            System.out.println("游泳" + i);
            if( i == 20 ){
                System.out.println("上岸?? (y/n)");
                String choice = new Scanner(System.in).nextLine();
                if( "Y".equalsIgnoreCase(choice) ){
                    break;
                }else
                    return "String : 淹死了。。。。。";
            }
        }
        return "OK";
    }
}

创建线程方式4:匿名内部类

public static void main(String[] args) {
    
    for(int i =0; i<400 ; i++){
        System.out.println("main  i="+i );
        if( i == 100 ){
            //创建新线程
            new Runnable() {                    
                @Override
                public void run() {
                    for(int j=0; j<100 ; j++){
                        System.out.println("新线程  j="+j );
                    }
                }
            };
        }
    }
}

一般推荐采用实现接口的方式来创建多线程

三、线程的构造方式:

无论是通过Thread类还是Runnable接口建立线程,都必须建立Thread类或它的子类的实例。Thread类的构造方法被重载了八次,构造方法如下:

public Thread( );
public Thread( Runnable target );
public Thread( String name );
public Thread( Runnable target, String name );
public Thread( ThreadGroup group, Runnable target );
public Thread( ThreadGroup group, String name );
public Thread( ThreadGroup group, Runnable target, String name );
public Thread( ThreadGroup group, Runnable target, String name, long stackSize );

Runnable target
实现了Runnable接口的类的实例。要注意的是Thread类也实现了Runnable接口,因此,从Thread类继承的类的实例也可以作为target传入这个构造方法。
String name
线程的名子。这个名子可以在建立Thread实例后通过Thread类的setName方法设置。如果不设置线程的名子,线程就使用默认的线程名:Thread-N,N是线程建立的顺序,是一个不重复的正整数。
ThreadGroup group
当前建立的线程所属的线程组。如果不指定线程组,所有的线程都被加到一个默认的线程组中。一旦线程加入了某个线程组,那么该线程就一直存在于该线程组,不能修改直到死亡。
long stackSize
线程栈的大小,这个值一般是CPU页面的整数倍。如x86的页面大小是4KB。在x86平台下,默认的线程栈大小是12KB。

四、线程的生命周期:

线程是一个动态执行的过程,它也有一个从产生到死亡的过程。

  1. 新建状态( new )
    使用new创建一个线程对象,仅仅在堆中分配内存空间,在调用start方法之前新建状态下,线程压根就没有启动,仅仅只是存在一个线程对象而已.
    Thread t= new Thread(): //此时t就属于新建状态
    当新建状态下的线程对象调用了start方法,此时从新建状态进入可运行状态。
    线程对象的start方法只能调用一次否则报错:llegalThreadstateException.
  2. 可运行状态( runnable )
    分成两种状态,readyrunning。分别表示就绪状态和运行状态。
    就绪状态线程对象调用start方法之后,等待JM的调度(此时该线程并没有运行)
    运行状态线程对象获得JVM调度,如果存在多个CPU,那么允许多个线程并行运行。
  3. 阴塞状态( blocked )
    正在运行的线程因为某些原因放弃CPU,暂时停止运行,就会进入阻塞状态。此时JVM不会给线程分配CPU,直到线程重新进入就绪状态才有机会转到运行状态。
    阻塞状态只能先进入就绪状态,不能直接进入运行状态。
    阳塞状态的两种情况:
    1. 当A线程处于运行过程时,试图获取同步锁时,却被B线程获取此时JvM把当前A线程存到对象的锁池中,A线程进入阳塞状态。
    2. 当线程处于运行过程时,发出了10请求时,此时进入阳塞状态
  4. 等待状态(waiting)
    等待状态只能被其他线程唤醒。此时使用的无参数的wait
    当线程处于运行过程时,调用了wait()方法,此时JvM把当前线程存在对象等待池中。
  5. 计时等待状态(timed waiting)
    使用了带参数的wait方法或者sleep方法
    1):当线程处于运行过程时,调用了wait(ong time)方法此时VM把当前线程存在对象等待池中.
    2):当前线程执行了sleep(long time)方法
  6. 终止状态(terminated)通常称为死亡状态,表示线程终止.
    1):正常执行完run方法而退出(正常死亡)
    2):遇到异常而退出(出现异常之后,程序就会中断(意外死亡).
    线程一旦终止,就不能再重启启动,否则报错(llegalhreadStateException).
    在Thread类中过时的方法:因为存在线程安全问题,所以弃用了
    void suspend():暂停当前线程
    void resume):恢复当前线程
    void stopl):结束当前线程

五、解决线程安全性-锁

这里介绍三种方式:

  • 同步代码块
  • 同步方法
  • 锁机制(Lock)

为了保证每个线程都能正常执行原子操作。Java引入了线程同步机制(也叫同步监听对象同步锁 / 同步监听器 / 互斥锁)

对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁,一般的,我们把当前并发访问的共同资源作为同步监听对象
注意:在任何时候最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着。

任何对象都可以作为同步监听对象。

synchronized 锁住的是括号里的对象,而不是代码。
对于非static的synchronized方法,锁的是对象本身也就是this。
当synchronized锁住一个对象后,别的线程如果也想拿到这个对象的锁,就必须等待这个线程执行完成释放锁,才能再次给对象加锁,这样才达到线程同步的目的。
即使两个不同的代码段,都要锁同一个对象,那么这两个代码段也不能在多线程环境下同时运行。
所以我们在用synchronized关键字的时候,能缩小代码段的范围就尽量缩小,能在代码段上加同步就不要再整个方法上加同步。这叫减小锁的粒度,使代码更大程度的并发。

1.同步代码块:
//synchronized(同步对象){
//    //需要同步的代码块
//}
class apple1 implements Runnable{
    private int apples = 100;
    @Override
    public void run() {
        
        for( int i=0; i<=100; i++ ) {
            synchronized (this) { /** <this>表示apple1的对象,该对象属于多线程共享的资源 **/
            
                apples--;
                if( apples > 0 ) {                  
                    try {
                        Thread.sleep(50);
                    } 
                    catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println( Thread.currentThread().getName() + "吃了第" + apples + "个" );
                }
            }
        }
    }
}

2.同步方法 (使用Synchronized修饰的方法 )

保证线程A执行该方法时,其他线程只能在方法外等着。

对于非static方法,同步锁就是this,即对象本身。

对于static方法,同步锁是当前方法所在类的字节码对象(类XX.class)

不要使用synchronized修饰run方法,修饰之后,某一个线程就执行完了所有的功能,好比是多个线程出现串行。
解决方案:
把需要同步操作的代码定义在一个新的方法中。
并且该方法使用synchronized修饰,再在run方法中调用该新的方法即可。

class apple2 implements Runnable{
    private int apples = 100;
    @Override
    public void run() {
        for( int i=0; i<=100; i++ ) {
                eat();
        }
    }
    
    synchronized private void eat() {
        apples--;
        if( apples > 0 ) {                  
            try {
                Thread.sleep(20); //模拟网络延迟
            } 
            catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println( Thread.currentThread().getName() + "吃了第" + apples + "个" );
            // Thread.currentThread().getName() :拿到当前线程的名称 
        }
    }
}

3.同步锁机制 (Lock)

Lock机制提供了比synchronized代码块或方法更广泛的锁定操作,同步代码块/方所具有的功能Lock都具有。且更强大,更体现面向对象。

Lock机制synchronized修饰的代码块 / 方法的区别:
从语义功能上来说没区别。lock更体现面向对象,synchronized会自动获取和释放同步锁,但Lock接口没有锁这个对象,需要手动获取和释放;

Comments

No comments yet. Why don’t you start the discussion?

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注