`
new_Line
  • 浏览: 9766 次
  • 性别: Icon_minigender_1
最近访客 更多访客>>
社区版块
存档分类
最新评论

黑马程序员_java多线程

 
阅读更多

------- android培训java培训、期待与您交流! ----------

进程:
    是一个正在运行中的程序,每一个进程执行,都有一个执行顺序,该顺序是一个执行路径,或者叫一个控制单元
线程:
    进程中的一个独立的控制单元,线程在控制着进程的执行,一个进程中至少有一个线程(控制单元)
JVM:   
    运行时会有一个进程叫做java.exe,该进程中至少一个线程负责java程序的执行,而且这个程序的代码存在于main方法中该线程称之为主线程。
        扩展:其实更细节的说明JVM,JVM启动时不止一个线程,还有一个垃圾回收机制的线程
如何在自定义的代码中自定义一个线程呢?
    通过API文档,java已经提供了对线程这类事物的描述,就是Thread类。
继承Thread类(用于描述线程)
    步骤:
        1、首先,定义类继承Thread;
        2、复写Thread类中的run方法;目的:将自定义代码存储在run方法中,让线程执行
        3、调用线程的start方法(该方法有两个作用:启动线程和调用run()方法)
    示例:
        public class Test1 {
            public static void main(String[] args)
            {
                Demo d = new Demo();  //创建线程
                d.start();  //开启线程并执行该线程中的run方法
                //d.run();  //为什么不是调用该方法,因为调用该方法,虽然线程是创建了
                //但是并没有开启线程,所以还是单线程,按顺序执行代码
                for(int i = 0; i < 40; i++)
                    System.out.println("主线程的:-----" + i);
            }
        }
        class Demo extends Thread
        {
            public void run()
            {
                for(int i = 0; i < 40; i++)
                    System.out.println("子类的:----" + i);
            }
        }
    为什么要复写run方法呢?
        因为thread类定义了一个用于存储线程要运行的代码的方法,该方法就是run。
            也就是说Thread类中的run方法,用于存储线程要运行的代码,主线程中的代码存储于main方法中
    线程中的方法:
        Thread.currentThread()---->获取当前线程的名称
         getName()---->获取线程的名称
         setName()或者super(name)---->设置线程的名称
实现Runnable接口。
    步骤:
        1、定义类实现Runnable接口
        2、复写接口中的run方法
        3、创建实现Runnable接口对象
        4、创建Thread类对象,将实现Runnable接口的对象作为实际参数传递给Thread类的构造函数
        5、调用Thread类的start方法,开启接口
    示例:
        class MyRunnable implements Runnable
        {
            public void run()
            {
                for(int i = 0; i < 10; i++)
                {
                    System.out.println(Thread.currentThread() + "---->" + i);
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        public class Demo {
            public static void main(String[] args) throws InterruptedException
            {
                MyRunnable mb = new MyRunnable();
                Thread t = new Thread(mb);
                t.start();
                for(int i = 0; i < 22; i++)
                {
                    System.out.println(Thread.currentThread() + "---->" + i);
                    Thread.sleep(200);
                }
            }
        }
继承Thread类和实现Runnable接口有什么区别?
  继承Thread类:线程代码存放在Thread子类的run方法中
  实现Runnable:线程代码存放在Runnable实现类的run方法中
  实现Runnable避免了单继承的局限性,在定义线程时建议使用实现方式来完成
线程的生命周期:
    线程的生命周期包含状态:新状态、就绪状态、阻塞状态、死亡状态
    新状态:
        Thread t = new Thread();
        刚刚new出来的线程对象,还没有调用start()方法。
    就绪状态(可运行状态):
        t.start()之后,线程进入就绪状态!在某一个时间点上,只有一个线程是运行着的,其它的线程都没有运行,
        所以我们说start()之后不是“运行状态”,而是“就绪状态”。
        线程什么时候从就绪到运行,这由CPU来决定,?CPU会给运行的线程一个时间片,这个时间用完,
        如果线程还没有主动阻塞,那么CPU会强行停止运行,给其他线程运行机会。
    运行状态:
        由CPU决定,CPU会在就绪状态的线程中随机选择一个,给其运行的时间片。
        运行的线程,应该主动进入阻塞状态,这样给其他线程运行的时间。
    阻塞状态:
        睡眠:Thread.sleep(1000),当线程执行了sleep()方法,那么这个线程就进入了休眠状态。
              休眠的线程必须要等到指定的毫秒过完,才能返回到就绪状态。
        等待:当前线程执行了wait()方法,进入了对象的等待列表中。
              只能期待其他线程调用notify()或notifyAll()来唤醒这个等待的线程。
        IO阻塞:当线程正在完成IO操作,那么这个线程也就阻塞了。直到IO操作完成了!
        锁定: 当线程要使用的对象被其他线程使用时,那么这个线程进入了锁定状态。
               直到线程得到了要使用的对象后,那么就回到就绪状态。
    死亡状态:
        run()方法结束,正常死亡!
        run()中抛出了异常,因为而死亡!
线程中的常用方法:
    static Thread currentThread():获取当前线程的对象,相当于this
    String getName():获取当前线程的名称
    设置线程名称:void setName(String name)或通过构造器
当多个线程共享同一个数据时,导致共享数据出现错误。
解决方案:一个线程执行中,其它线程不能参与执行
Java对于多线程的安全问题,提供了专业的解决方案:
    同步代码块:
        synchronized(监视器对象)
        {
            //需要被同步的代码
        }
        虽然Java允许使用任何对象作为监视器对象,但想一下同步的目的:阻止多个线程对同一个共享资源并发访问,
        因此通常使用可能被并发访问的资源作为同步监视器对象。
        案例体现:模拟银行取钱操作
           
            package com.itheima.bank;
            public class Account {
                //账户余额
                private double balance;
                public Account(double balance) {
                    this.balance = balance;
                }
                /*取钱的方法*/
                public double drawBalance(double drawBalance)
                {
                    balance = balance - drawBalance;
                    return balance;
                }
                /*查询余额*/
                public double getBalance()
                {
                    return balance;
                }
            }

            package com.itheima.bank;
            public class DrawThread extends Thread
            {
                private Account a;
                //要取的金额
                private double drawBalance;
                public DrawThread(String name, Account a, double drawBalance)
                {
                    super(name);
                    this.a = a;
                    this.drawBalance = drawBalance;
                }
                public void run()
                {
                    while(true){
                        /*同步代码块,将共享访问的Account对象作为锁*/
                        synchronized(a){
                            if(a.getBalance() < drawBalance){
                                System.out.println(Thread.currentThread() + " : 余额不足,当前余额: " + a.getBalance());
                                break;
                            }
                            try {
                                Thread.sleep(10);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            a.drawBalance(drawBalance);
                            System.out.println(Thread.currentThread() + "取钱成功,取走金额:" + drawBalance + " ,当前余额:" + a.getBalance());
                        }
                    }
                }
            }

            package com.itheima.bank;
            public class DrawTest {
                public static void main(String[] args) {
                    Account a = new Account(1000);
                    new DrawThread("A账户:", a, 500).start();
                    new DrawThread("B账户:", a, 300).start();
                }
            }
    同步方法:
        同步方法是使用synchronized关键字来修饰某个方法,则该方法就被称为同步方法。
            例如:public synchronized void show(){}
        对于同步方法而言,无需显示指定同步监视器对象,同步方法的同步监视器对象是this。
        同步方法的特点:一个对象的一个同步方法被调用,它的其它同步方法都用不了
        案例体现:
            package com.itheima;
            public class SynDemo1 {
                public static void main(String[] args) {
                    A a = new A();
                    Thread t1 = new Thread1(a);
                    Thread t2 = new Thread2(a);
                    t1.start();
                    t2.start();
                }
            }
            class Thread1 extends Thread {
                private A a;
                public Thread1(A a) {
                    this.a = a;
                }
                public void run() {
                    a.fun1();
                }
            }

            class Thread2 extends Thread {
                private A a;
                public Thread2(A a) {
                    this.a = a;
                }
                public void run() {
                    a.fun2();
                }
            }
            class A {
                public synchronized void fun1() {
                    System.out.println("fun1进来了");
                    System.out.println("fun1()");
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("fun1出去了");
                }
               
                public synchronized void fun2() {
                    System.out.println("fun2进来了");
                    System.out.println("fun2()");
                    System.out.println("fun2出去了");
                }
            }
            打印结果:fun1进来了
                  fun1()
                  fun1出去了
                  fun2进来了
                  fun2()
                  fun2出去了           
        对象如同锁,持有锁的线程才能在同步中执行,没有锁的线程,即使获取cpu的执行权,也进不去
        同步的前提:必须要有两个或以上的线程访问共享数据;必须是多个线程使用同一个锁(比如火车上的两个卫生间,就不是同一个锁)
        好处:解决了多线程安全问题
        弊端:多个线程都需要判断锁,较为消耗资源
        使用同步块还是使用同步方法呢?一般使用同步块,灵活
    静态同步方法:
        静态同步方法使用的锁是该方法所在类的class对象。即类名.class。
    单例设计模式:
        饿汉式:
            public class Single{
                //私有化构造器
                private Single(){}
                //创建对象
                private static final Single s = new Single();
                //提供方法,供外部使用
                public static Single getSingle()
                {
                    return s;
                }
            }
        懒汉式:
            public class Single(){
                private Single(){}
                private static Single s = null;
                public static Single getSingle()
                {
                    if(s == null)
                    {
                        synchronized(Single.class)
                        {
                            if(s == null)
                                s = new Single();
                        }
                    }
                }
            }
        懒汉式和饿汉式的区别:懒汉式的特点在于延迟加载,如果多个线程访问时,会出现安全问题。
                可以加同步来解决,用同步代码块和同步方法都行,但是同步方法稍微有些低效,
                所以一般用同步代码块,通过双重if判断来解决效率问题,同步锁是该类所属的字节码文件对象
    死锁:
        当两个线程相互等待对方释放同步监视器时就会发生死锁,Java虚拟机没有检测,也没有采取措施来处理死锁情况,
        所以多线程应该避免死锁的出现。一旦程序出现死锁,整个程序既不会发生异常,也不会有任何提示,
        只是所有线程都处于阻塞状态。一般这种情况发生在:同步中嵌套同步,但锁不同
        比如说:a线程锁定一个资源,同时想获取b线程的资源
            b线程锁定一个资源,同时想获取a线程的资源。
        举例:
            package com.itheima.lock;
            public class LockDemo {
                public static void main(String[] args) {
                    Thread t1 = new Thread1();
                    Thread t2 = new Thread2();
                    t1.start();
                    t2.start();
                }
            }
            class Thread1 extends Thread {
                public void run() {
                    synchronized (Object.class) {
                        System.out.println(Thread.currentThread().getName() + "...外层锁");
                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        synchronized (String.class) {
                            System.out.println("haha,卡住了");
                        }
                    }
                }
            }
            class Thread2 extends Thread {
                public void run() {
                    synchronized (String.class) {
                        System.out.println(Thread.currentThread().getName() + "...外层锁");
                        synchronized (Object.class) {
                            System.out.println("haha,我也卡住了");
                        }
                    }       
                }
            }   
    线程通信:
        当多个线程访问同一资源,每个线程的实现功能不一样,而每个线程之间需要协调发展(两种线程交替执行)。
        比如说:
              老师:我们开始上课;
              学生A:老师等一下,我要去厕所;
              老师:OK,你快点,那我wait()了,等你回来notify我一下。
        线程通信的前提:同步环境!
        为了实现这种功能,需要借助Object类提供的wait(),notify(),notifyAll()这3个方法,这些方法不是Thread类的方法。
        这三个方法都使用在同步中,因为要对持有监视器的锁进行操作,所以要定义在同步中,因为只有同步才具有锁的概念
        为什么操作线程的方法要定义在Object类中?
            因为这些方法在操作同步中线程时都必须要标识它们所操作线程持有的锁,
            只有同一个锁上的被等待线程可以被同一个锁上的notify唤醒,不可以对不同锁中的线程进行唤醒
            也就是说:等待和唤醒必须是同一把锁
        而锁可以是任意对象,所以把可以被任意对象掉用的wait(),notify(),notityAll()定义在Object类中
        wait():声明了InterruptedException异常!在使用wait()时,还要使用try/catch。
        举例说明:
            package com.itheima.bank1;
            public class Bank {
                private double money;
                boolean flag = false;
                public Bank(double money)
                {
                    this.money = money;
                }
                /*取钱的方法*/
                public double drawMoney(double drawMoney)
                {
                    money = money - drawMoney;
                    return money;
                }
                /*存钱的方法*/
                public double saveMoney(double saveMoney)
                {
                    money = money + saveMoney;
                    return money;
                }
                /*查询余额*/
                public double getMoney()
                {
                    return money;
                }
            }
            package com.itheima.bank1;
            /*
             * 取钱操作的线程
             * */
            public class DrawThread extends Thread{
                private Bank b;
                private double drawMoney;
                public DrawThread(Bank b, double drawMoney, String name)
                {
                    super(name);
                    this.b = b;
                    this.drawMoney = drawMoney;
                }
                public void run()
                {
                    while(true)
                    {
                        synchronized(b)
                        {
                            if(!b.flag)
                            {
                                try {
                                    b.wait();
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }
                            }
                            if(b.getMoney() < drawMoney){
                                System.out.println("余额不足,当前余额是:" + b.getMoney());
                                break;
                            }
                            b.drawMoney(drawMoney);
                            System.out.println(Thread.currentThread().getName() + " 取款成功,取款金额是:" + drawMoney + ",账户余额:" + b.getMoney());
                            b.flag = false;
                            b.notify();
                        }
                    }
                }
            }
            package com.itheima.bank1;
            /*
             * 存钱操作的线程
             * */
            public class SaveThread extends Thread {
                private Bank b;
                private double saveMoney;
                public SaveThread(Bank b, double saveMoney, String name)
                {
                    super(name);
                    this.b = b;
                    this.saveMoney = saveMoney;
                }
                public void run()
                {
                    while(true)
                    {
                        synchronized(b)
                        {
                            if(b.flag)
                            {
                                try {
                                    b.wait();
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }
                            }
                            b.saveMoney(saveMoney);
                            System.out.println(Thread.currentThread().getName() + "存款成功,存款金额:" + saveMoney + ",当前余额:" + b.getMoney());
                            b.flag = true;
                            b.notify();
                        }
                    }
                }
            }
            package com.itheima.bank1;
            public class BankTest {
                public static void main(String[] args) {
                    Bank b = new Bank(5000);
                    new SaveThread(b, 200, "存钱线程...").start();
                    new DrawThread(b, 500, "取钱线程...").start();
                }
            }
    线程通信小结:
        1、使用wait()、notify()、notifyAll()方法可以完成线程间的通讯,可叫它们通讯方法;
        2、只能在同步环境下调用通讯方法;         
        3、只能使用监视器对象调用通讯方法;
        4、每个监视器对象都有一个线程监狱:执行监视器对象.wait()的线程会被关押到监视器对象的线程监狱中;
        5、若想释放出锁对象的线程监狱中的线程,那么需要调用监视器对象.notify()方法,
           该方法只能保证在监视器对象的线程监狱中释放出一个线程,但不能保证释放的是哪一个;
        6、还可以使用监视器.notifyAll()方法释放出监视器对象的监狱中关押的所有线程。
        7、被wait()了的线程不能自己恢复到就绪状态,只能等待其他线程调用同一监视器对象上的notify()或notifyAll()方法来唤醒。
        8、被wait()了的线程会释放监视器对象的对象锁,这样其他线程就可以进入他占用的同步环境。
        9、被唤醒的线程恢复到了就绪状态,当再次获取监视器对象的锁后会在wait()处向下运行。
    后台线程(守护线程):
       
    停止线程:
        如何停止线程呢?
            只有一种,run()结束
        怎么让run()结束呢?
            开启多线程运行,通常是通过循环结构,只要控制住循环,就可以让run()结束,也就是线程结束
        特殊情况:当线程处于冻结状态,线程就不会结束
        当没有指定的方式让线程从冻结恢复到运行状态,这是需要对冻结进行清除,强制让它恢复到运行状态中来,
        这样就可以操做标记让线程结束
        interrupt():将处于冻结状态的线程,强制的恢复到运行状态
    守护线程(后台线程):
        void setDaemon(boolean on):将该线程标记为守护线程或用户线程。
        将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。
        该方法必须在启动线程前调用。
        后台线程的子线程,也是默认为后台线程!
          什么叫子线程?A线程的任务中启动了B线程,那么B是A的子线程。
        举例说明:
            package com.itheima;
            /*
             * 演示后台线程!
             */
            public class ServerThread {
                public static void main(String[] args) throws InterruptedException {
                    Thread t = new MyThread();
                    t.setDaemon(true);//在start()之前把t设置为后台线程
                    t.start();
                    for(int i = 0; i < 3; i++) {
                        System.out.println(i);
                        Thread.sleep(300);
                    }
                    System.out.println("bye-bye!");
                }
            }
            class MyThread extends Thread {
                public void run() {
                    for(int i = 0; i < 10; i++) {
                        Thread t = new ZiThread();
                        t.start();
                    }
                }
            }
            class ZiThread extends Thread {
                public void run() {
                    while(true) {
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
            运行结果:
                0
                1
                2
                bye-bye!

    合并线程(join):
         void join():等待该线程终止。
         join()方法的作用:使当前线程等待该线程结束再向下运行。
         当A线程执行到了B线程的.join()方法时,A线程就会等待B线程执行完,才继续执行
         join()可以用来临时加入线程执行
         演示:
            package com.itheima;
            /*
             * 演示join()
             */
            public class JoinDemo {
                public static void main(String[] args) throws InterruptedException {
                    Thread t = new JoinThread();
                    t.start();
                    System.out.println("我要等待t结束");
                    t.join();//当前线程是主线程,主线程要等待t结束
                    System.out.println("\n终于等到t结束了!");
                }
            }
            class JoinThread extends Thread {
                public void run() {
                    for(int i = 0; i < 10; i++) {
                        System.out.print(i + " ,");
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
            运行结果:
                我要等待t结束
                0 ,1 ,2 ,3 ,4 ,5 ,6 ,7 ,8 ,9 ,
                终于等到t结束了!
    线程让步(yield):
        在以前使用sleep()的地方就可以尝试使用yield
        yield()方法的作用:说线程已经把最重要的工作做完了,告诉CPU可以切换给其他线程了。
        让步不同于阻塞:让步的线程没有进入阻塞状态,只是从运行状态到就绪状态!

------- android培训java培训、期待与您交流! ----------

分享到:
评论

相关推荐

    黑马程序员_张孝祥_Java多线程与并发库 视频+代码+资料

    黑马程序员_张孝祥_Java多线程与并发库,视频+代码+资料

    黑马程序员_张孝祥_Java多线程与并发库

    黑马程序员_张孝祥_Java多线程与并发库,老师讲的非常仔细,老师很有耐心.欢迎大家下载学习.

    黑马程序员-java多线程技术01

    NULL 博文链接:https://huangminwen.iteye.com/blog/1157983

    传智播客.黑马程序员《Java 基础入门》课后习题答案

    1、 面向对象、跨平台性、健壮性、安全性、可移植性、多线程性、动态性等。 2、 JRE(Java Runtime Environment,Java 运行时环境),它相当于操作系统部分,提供了 Java 程序运 行时所需要的基本条件和许多 Java ...

    黑马程序员–Java多线程讲解笔记

    当一个进程中线程有多个时,是多线程。  为什么要用多线程  1,让计算机"同时"做多件事情,节约时间。  2,后台运行程序,提高程序的运行效率.。  3,多线程可以让程序"同时"处理多个事情。  4,...

    传智播客_Java培训_毕向东_Java基础[05-多线程]

    传智播客_Java培训_毕向东_Java基础[05-多线程]系黑马程序员_毕向东_Java基础视频教程

    java并发库高级应用源码--张孝祥

    java并发库thread使用,传统线程技术、定时器技术、线程互斥技术,同步通讯技术、多线程共享数据、并发库应用,线程锁技术,阻塞锁、阻塞队列,线程池等应用

    图解java多线程设计模式

    java.util.concurrent包、synchronized关键字、Swing框架、Java内存模型等内容也均有涉及,不仅能够了解Java多线程的相关知识,还可加深对Java语言的理解。 本书适合以下读者阅读 a.对多线程感兴趣的人 b.对Java...

    图解java多线程设计模式2017年8月最新版

    264张图表 + 300段Java示例程序 = 轻松学习多线程编程 日本经典多线程入门书,原版长销11年! 本书适合以下读者阅读 a.对多线程感兴趣的人 b.对Java编程感兴趣的人 c.对设计模式感兴趣的人 d.对面向对象开发感兴趣的...

    java拼图游戏源码.zip

    这是一个java写的拼图游戏,用了swing、多线程等知识,可以设置行数列、列数、还有使用多线程写的游戏动画,拼图的图块具有磁贴设计,代码封装性较强,容易移植,纯原创。

    《Java基础案例教程(第2版)》课后习题答案1

    第1章 Java开发入门一.填空题1. 面向对象,SUN 2. JavaSE,JavaEE,JavaME3.面向对象、跨平台性、支持多线程4. JDK5.bin

    黑马程序员 安卓学院 万元哥项目经理 分享220个代码实例

    |--利用FinalHttp实现多线程断点续传 |--加密之MD5 |--动画Animation详解 |--动画之view左右抖动 |--动画之移动动画 |--动画之组合动画 |--动画之缩放动画ScaleAnimation |--反序列化对象 |--发送短信 读天气 调音量...

    图解java多线程设计模式 2017年8月最新版

    这本书的pdf文件有162M,是最新版高清的pdf,网上原书售价60多元,这里提供的附件是百度云的链接地址。

    拼图游戏 (源码+所有文件)

    萌新做的一个简单的Java拼图游戏 启动类:pers.sept.jigsaw1.swing下的...多线程。 下拉列表选择数字可以切换关卡图片,最后的“+”,可以添自己的图片到关卡中。 设有背景音乐 有一键通过按钮 等等,块下载去看看吧。

    JAVA核心知识点整理.pdf

    给大家分享一篇我在学习java过程中...包含常见的面试题:JVM、Java集合、多线程并发、java基础、SSM框架原理、微服务、Netty和RPC、网络、日志、算法、数据结构、加密、分布式算法、机器学习、大数据等知识点,都是干货

    java7源码-Java-concurrent:Java并发编程

    黑马程序员——Java异步课程源码 plugin Lombok logback.xml 说明 课堂代码主要在 case_java8 子模块内,按章节划分, io.github.hank.java.concurrent.n2 - 同步与异步 io.github.hank.java.concurrent.n3 - Java...

    Java基础最全笔记文档

    Java基础笔记分为 Java基础篇 和 Java加强篇 Java基础篇包括: 1. Java环境搭建、Java快速入门、IDEA开发工具 ...11. 多线程 12. 网络编程 13. 单元测试、反射、注解、动态代理 14. XML、解析、工厂模式和装饰模式

    黑马乐优商城项目后台管理系统.zip

    多线程支持: Java内置了对多线程的支持,允许程序同时执行多个任务。这对于开发需要高并发性能的应用程序(如服务器端应用、网络应用等)非常重要。 自动内存管理(垃圾回收): Java具有自动内存管理机制,通过...

    学成在线-分布式任务视频处理

    2.适用于黑马程序员Java企业级实战开发《学成在线》微服务项目,基于SpringCloud、SpringCloudAlibaba技术栈开发。 3.采用xxl-job技术。 4.步骤概要: 4.1 作业分片方案 4.2 保证任务不重复执行 4.3 业务流程 5....

    黑马JVM学习笔记二

    title: 黑马JVM学习笔记二 date: 2020-01-13 1:00:00 ...线程是私有的,多线程之间分别有各自的程序计数器记录对应线程的执行位置 程序计数器是Java虚拟机规范中唯一一个不会存在内存溢出的区(堆和栈等会出现内存溢

Global site tag (gtag.js) - Google Analytics